Skip to content

Commit

Permalink
Update swedish kaifa meter format. (#81)
Browse files Browse the repository at this point in the history
* Update swedish kaifa meter format.

* Update power sensor in swedish kaifa units

* REsolve problem in manual configuration.

* Correct the CONFIG_SCHEMA

* Correct the CONFIG_SCHEMA cv type

* set SCHEMA correctly.
add debug to serial ports.
Update README.md

* Update README.md

* Update parser code

* Update config_flow for manual port entry

* Typo

* Update README.md

Add My badge to replace HACS custom

* Add logo and top level headers (#82)

* https://openclipart.org/detail/282493/building-automation-controller

https://openclipart.org/download/282493/bus-gateway.svg

* https://openclipart.org/detail/286510/simple-meter-indicator-icon

https://openclipart.org/download/286510/simple_meter_icon.svg

* https://openclipart.org/detail/204609/black-man-1

https://openclipart.org/download/204609/black-man-1.svg

* https://openclipart.org/detail/204610/black-man-2

https://openclipart.org/download/204610/black-man-2.svg

* https://openclipart.org/detail/46429/arrow-right-black-outline

https://openclipart.org/download/46429/Arrow-002.svg

* https://github.com/home-assistant/assets/raw/master/logo/logo.svg

* Create logo from the other svg files

* Export 800px wide png

* Markdownlint fixes

* Update module links to use product name

* Upcase Norwegian and Swedish

* Add links

* Format parameter description

* Add top level headings

* Add logo to readme

* Update swedish kaifa meter format.

* Update power sensor in swedish kaifa units

* REsolve problem in manual configuration.

* Correct the CONFIG_SCHEMA

* Correct the CONFIG_SCHEMA cv type

* set SCHEMA correctly.
add debug to serial ports.
Update README.md

* Update parser code

* Update config_flow for manual port entry

* Typo

* Update README.md

* Update README.md

Co-authored-by: hlovdal <kode@denkule.no>
  • Loading branch information
turbokongen and hlovdal authored Oct 15, 2022
1 parent e4cad71 commit 99d4d17
Show file tree
Hide file tree
Showing 13 changed files with 208 additions and 393 deletions.
29 changes: 22 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

![logo](logo_images/logo.png)

[![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge)](https://github.com/custom-components/hacs)
[![hacs_badge](https://img.shields.io/badge/HACS-Default-41BDF5.svg?style=for-the-badge)](https://github.com/hacs/integration)
<a href="https://www.buymeacoffee.com/turbokongen" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>

Custom component reading [AMS](https://no.wikipedia.org/wiki/Smart_str%C3%B8mm%C3%A5ler)
Expand Down Expand Up @@ -80,22 +80,37 @@ Then setup via *Integrations* config.
### YAML options

```yaml
#Serial port example
ams:
protocol: serial # Required. The Protocol type for communications.
serial_port: '/dev/ttyUSB0' # Required. The serial port used to communicate through
baudrate: 2400 # Optional, defaults to '2400'
parity: 'N' # Optional, defaults to 'N'
meter_manufacturer: 'auto' # Optional, defaults to 'auto'
```
Start Home-Assistant and
set up the integration in the *Integrations* config if you haven't set up by YAML config.
For `parity` values see <https://github.com/pyserial/pyserial/blob/master/serial/serialutil.py#L79>
```yaml
# TCP/IP config example
ams:
protocol: tcp_ip #Required. The protocol type for communications.
tcp_host: 192.168.2.11 # Required. The transmitting host address.
tcp_port: 8900 #Required. The transmitting host port.
meter_manufacturer: 'kamstrup' # Optional, defaults to 'auto'
```
*All options:*
```yaml
protocol: Options are 'tcp_ip' or 'serial' This is option is required.
serial_port: Input your serial port to communicate through. Required if 'serial' is selected for 'protocol'.
baudrate: Input a custom baudrate. Default is 2400. This option in optional.
parity: Input a custom parity option. Default is 'N'. See https://github.com/pyserial/pyserial/blob/master/serial/serialutil.py#L79
tcp_host: Ip adress to host of meter data. Required if 'tcp_ip' is selected
tcp_port: Port at host of meter data. Required if 'tcp_ip' is selected.
meter_manufacturer: Set the meter manufacturer if 'auto' fails. This option is optional.
```
For `meter_manufacturer` values the options are:

```python
'auto'
'auto' # This is default if nothing is specified.
'aidon'
'aidon_se' # Swedish aidon meter RF2 modules
'kamstrup'
Expand Down
107 changes: 65 additions & 42 deletions custom_components/ams/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from custom_components.ams.parsers import kaifa as Kaifa
from custom_components.ams.parsers import kamstrup as Kamstrup
from custom_components.ams.parsers import aidon_se as Aidon_se
from custom_components.ams.parsers import kaifa_se as Kaifa_se
from custom_components.ams.const import (
AMS_DEVICES,
AIDON_METER_SEQ,
Expand All @@ -24,6 +23,7 @@
CONF_METER_MANUFACTURER,
CONF_PARITY,
CONF_PROTOCOL,
CONF_PROTOCOL_TYPE,
CONF_SERIAL_PORT,
CONF_TCP_HOST,
CONF_TCP_PORT,
Expand All @@ -44,27 +44,34 @@
SENSOR_ATTR,
SERIAL,
SIGNAL_NEW_AMS_SENSOR,
SIGNAL_UPDATE_AMS
SIGNAL_UPDATE_AMS,
)

_LOGGER = logging.getLogger(__name__)

SERIAL_SCHEMA = {vol.Required(CONF_SERIAL_PORT, default=DEFAULT_SERIAL_PORT)}
NETWORK_SCHEMA = {vol.Required(CONF_TCP_HOST), vol.Required(CONF_TCP_PORT)}
PROTOCOL_SCHEMA = {
vol.Required(SERIAL): SERIAL_SCHEMA,
vol.Required(NETWORK): NETWORK_SCHEMA,
}
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(
CONF_SERIAL_PORT, default=DEFAULT_SERIAL_PORT
): cv.string,
vol.Required(
CONF_METER_MANUFACTURER,
default=DEFAULT_METER_MANUFACTURER
): cv.string,
vol.Optional(CONF_PARITY, default=DEFAULT_PARITY):
cv.string,
vol.Required(CONF_PROTOCOL, default=SERIAL): vol.In([NETWORK, SERIAL]),
vol.Optional(CONF_TCP_HOST): str,
vol.Optional(CONF_TCP_PORT): vol.All(
vol.Coerce(int), vol.Range(0, 65535)
),
vol.Optional(CONF_SERIAL_PORT): str,
vol.Optional(CONF_PARITY, default=DEFAULT_PARITY): cv.string,
vol.Optional(CONF_BAUDRATE, default=DEFAULT_BAUDRATE): vol.All(
vol.Coerce(int), vol.Range(0, 256000)
),
vol.Optional(
CONF_BAUDRATE, default=DEFAULT_BAUDRATE
): vol.All(int),
CONF_METER_MANUFACTURER, default=DEFAULT_METER_MANUFACTURER
): cv.string,
}
)
},
Expand Down Expand Up @@ -98,8 +105,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up AMS as config entry."""
_setup(hass, entry.data)
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(
entry, "sensor"))
hass.config_entries.async_forward_entry_setup(entry, "sensor")
)
return True


Expand Down Expand Up @@ -184,11 +191,10 @@ def read_packet(self):
# Build packet
bytelist.extend(buf)
byte_counter = byte_counter + 1
if byte_counter == 3:
if byte_counter == 3:
# Calculate size after FRAME_FLAG + 2 bytes are
# received
packet_size = ((bytelist[1] & 0x0F) << 8 |
bytelist[2]) + 2
packet_size = ((bytelist[1] & 0x0F) << 8 | bytelist[2]) + 2
if byte_counter == packet_size:
# If we have built a packet equal to packet size
if buf == FRAME_FLAG:
Expand All @@ -197,23 +203,32 @@ def read_packet(self):
else:
# Not valid packet. Flush what we have built so
# far.
_LOGGER.debug("Not a valid packet. Start over "
"again. byte_counter=%s, "
"frame_started=%s, "
"packet_size=%s, DUMP: %s",
byte_counter, frame_started,
packet_size, bytelist)
_LOGGER.debug(
"Not a valid packet. Start over "
"again. byte_counter=%s, "
"frame_started=%s, "
"packet_size=%s, DUMP: %s",
byte_counter,
frame_started,
packet_size,
bytelist,
)
bytelist = []
byte_counter = 0
frame_started = False
packet_size = -1
else:
if frame_started:
_LOGGER.debug("Timeout waiting for end of packet. Flush "
" current packet. byte_counter=%s, "
"frame_started=%s, package_size=%s, "
"DUMP: %s", byte_counter, frame_started,
packet_size, bytelist)
_LOGGER.debug(
"Timeout waiting for end of packet. Flush "
" current packet. byte_counter=%s, "
"frame_started=%s, package_size=%s, "
"DUMP: %s",
byte_counter,
frame_started,
packet_size,
bytelist,
)
frame_started = False
byte_counter = 0
bytelist = []
Expand Down Expand Up @@ -242,14 +257,16 @@ def connect(self): # pylint: disable=too-many-branches
self.meter_manufacturer = self._find_parser(detect_pkg)
parser = self.meter_manufacturer

swedish = None
if self.meter_manufacturer == "aidon":
parser = Aidon
elif self.meter_manufacturer == "aidon_se":
parser = Aidon_se
elif self.meter_manufacturer == "kaifa":
parser = Kaifa
elif self.meter_manufacturer == "kaifa_se":
parser = Kaifa_se
parser = Kaifa
swedish = True
elif self.meter_manufacturer == "kamstrup":
parser = Kamstrup

Expand All @@ -261,8 +278,15 @@ def connect(self): # pylint: disable=too-many-branches
data = self.read_packet()
if parser.test_valid_data(data):
_LOGGER.debug("data read from port=%s", data)
self.sensor_data, _ = parser.parse_data(self.sensor_data,
data)
if swedish:
self.sensor_data, _ = parser.parse_data(
self.sensor_data, data, swedish
)
else:
self.sensor_data, _ = parser.parse_data(
self.sensor_data, data
)

self._check_for_new_sensors_and_update(self.sensor_data)
else:
_LOGGER.debug("failed package: %s", data)
Expand All @@ -281,7 +305,8 @@ def _test_meter(test_pkg, meter):
_LOGGER.debug("Testing for %s", meter)
for i, _ in enumerate(test_pkg):
if test_pkg[i] == meter[0] and (
test_pkg[i:(i + len(meter))] == meter):
test_pkg[i:(i + len(meter))] == meter
):
match.append(meter)
return meter in match

Expand Down Expand Up @@ -317,8 +342,7 @@ def missing_attrs(self, data=None):
if data is None:
data = self.data

attrs_to_check = [HAN_METER_SERIAL,
HAN_METER_MANUFACTURER, HAN_METER_TYPE]
attrs_to_check = [HAN_METER_SERIAL, HAN_METER_MANUFACTURER, HAN_METER_TYPE]
imp_attrs = [i for i in attrs_to_check if i not in self._attrs]
if imp_attrs:
cp_sensors_data = deepcopy(data)
Expand All @@ -329,9 +353,10 @@ def missing_attrs(self, data=None):
self._attrs[check] = val
break
del cp_sensors_data
miss_attrs = ([i for i in attrs_to_check if i not in self._attrs])
_LOGGER.debug("miss_attrs=%s", ([i for i in attrs_to_check
if i not in self._attrs]))
miss_attrs = [i for i in attrs_to_check if i not in self._attrs]
_LOGGER.debug(
"miss_attrs=%s", ([i for i in attrs_to_check if i not in self._attrs])
)
if miss_attrs:
_LOGGER.debug("We miss some attributes: %s", miss_attrs)
return True
Expand All @@ -349,12 +374,10 @@ def _check_for_new_sensors_and_update(self, sensor_data):
# use to create the unique_id
if self.missing_attrs(sensor_data) is True:
_LOGGER.debug(
"Missing some attributes waiting for new read from the"
" serial"
"Missing some attributes waiting for new read from the" " serial"
)
else:
_LOGGER.debug("Got %s new devices from the serial",
len(new_devices))
_LOGGER.debug("Got %s new devices from the serial", len(new_devices))
_LOGGER.debug("DUMP %s", sensor_data)
async_dispatcher_send(self._hass, SIGNAL_NEW_AMS_SENSOR)
else:
Expand Down
51 changes: 44 additions & 7 deletions custom_components/ams/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from custom_components.ams.const import ( # pylint: disable=unused-import
CONF_BAUDRATE,
CONF_MANUAL_SERIAL_PORT,
CONF_METER_MANUFACTURER,
CONF_PARITY,
CONF_PROTOCOL,
Expand All @@ -23,7 +24,7 @@
SERIAL,
)
DATA_SCHEMA_SELECT_PROTOCOL = vol.Schema(
{vol.Required("type"): vol.In([SERIAL, NETWORK])}
{vol.Required("type"): vol.In([SERIAL, CONF_MANUAL_SERIAL_PORT, NETWORK])}
)
DATA_SCHEMA_NETWORK_DATA = vol.Schema(
{
Expand Down Expand Up @@ -62,20 +63,56 @@ async def async_step_user(self, user_input=None):
if self.connection_type == NETWORK:
return await self.async_step_network_connection()
if self.connection_type == SERIAL:
return await self.async_step_serial_connection()
return await self.async_step_select_serial_connection()
if self.connection_type == CONF_MANUAL_SERIAL_PORT:
return await self.async_step_enter_serial_connection()

return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA_SELECT_PROTOCOL, errors=self._errors
)

async def async_step_serial_connection(self, user_input=None):
"""Handle the serialport connection step."""
async def async_step_enter_serial_connection(self, user_input=None):
"""Handle the manual serialport connection step."""

if user_input is not None:
user_input[CONF_PROTOCOL] = SERIAL
entry_result = self.async_create_entry(
title="AMS Reader", data=user_input,
)
if entry_result:
return entry_result

return self.async_show_form(
step_id="enter_serial_connection",
data_schema=vol.Schema(
{
vol.Required(
CONF_SERIAL_PORT, default=None
): vol.All(str),
vol.Required(
CONF_METER_MANUFACTURER,
default=DEFAULT_METER_MANUFACTURER
): vol.In(MANUFACTURER_OPTIONS),
vol.Optional(
CONF_PARITY, default=DEFAULT_PARITY
): vol.All(str),
vol.Optional(
CONF_BAUDRATE, default=DEFAULT_BAUDRATE
): vol.All(int),
}
),
errors=self._errors,
)

async def async_step_select_serial_connection(self, user_input=None):
"""Handle the select serialport connection step."""
portdata = await self.hass.async_add_executor_job(devices.comports)
_LOGGER.debug(portdata)
ports = [(comport.device + ": " + comport.description) for
comport in portdata]

if user_input is not None:
user_input[CONF_PROTOCOL] = self.connection_type
user_input[CONF_PROTOCOL] = SERIAL
user_selection = user_input[CONF_SERIAL_PORT]
port = portdata[ports.index(user_selection)]
serial_by_id = await self.hass.async_add_executor_job(
Expand All @@ -90,7 +127,7 @@ async def async_step_serial_connection(self, user_input=None):

_LOGGER.debug(ports)
return self.async_show_form(
step_id="serial_connection",
step_id="select_serial_connection",
data_schema=vol.Schema(
{
vol.Required(
Expand All @@ -114,7 +151,7 @@ async def async_step_serial_connection(self, user_input=None):
async def async_step_network_connection(self, user_input=None):
"""Handle the network connection step."""
if user_input:
user_input[CONF_PROTOCOL] = self.connection_type
user_input[CONF_PROTOCOL] = NETWORK
entry_result = self.async_create_entry(
title="AMS Reader", data=user_input,
)
Expand Down
8 changes: 5 additions & 3 deletions custom_components/ams/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,20 @@

CONF_BAUDRATE = "baudrate"
CONF_METER_MANUFACTURER = HAN_METER_MANUFACTURER
CONF_MANUAL_SERIAL_PORT = "manual_serial_port"
CONF_PARITY = "parity"
CONF_SERIAL_PORT = "serial_port"
CONF_TCP_PORT = "tcp_port"
CONF_TCP_HOST = "tcp_host"
CONF_PROTOCOL = "protocol"
CONF_PROTOCOL_CONFIG = "protocol_config"

CONF_PROTOCOL_TYPE = "type"
ATTR_DEVICE_CLASS = "device_class"
ATTR_LAST_RESET = "last_reset"
ATTR_STATE_CLASS = "state_class"
DEVICE_CLASS_ENERGY = "energy"
SERIAL = "Serial port"
NETWORK = "TCP/IP"
SERIAL = "serial"
NETWORK = "tcp_ip"
STATE_CLASS_TOTAL_INCREASING = "total_increasing"

DOMAIN = "ams"
Expand Down Expand Up @@ -120,6 +121,7 @@
"MA304H4": "Domestic/Industrial 3 Phase 400V 4-Wire meter",
"MA304T4": "Industrial 3 Phase 230V 3-Wire meter",
"MA304T3": "Industrial 3 Phase 400V 4-Wire meter",
"MA304H4D": "Poly Phase 3 Phase 230V/400V 4-Wire meter",
# Kamstrup
6861111: "Omnipower 1 Phase Direct meter",
6841121: "Omnipower 3 Phase 3-Wire Direct meter",
Expand Down
Loading

0 comments on commit 99d4d17

Please sign in to comment.