From 99d4d17ab8e97f37a3553fcfaf6dcc313c03720f Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Sat, 15 Oct 2022 08:21:11 +0200 Subject: [PATCH] Update swedish kaifa meter format. (#81) * 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 --- README.md | 29 +- custom_components/ams/__init__.py | 107 ++++--- custom_components/ams/config_flow.py | 51 +++- custom_components/ams/const.py | 8 +- custom_components/ams/manifest.json | 2 +- custom_components/ams/parsers/kaifa.py | 14 +- custom_components/ams/parsers/kaifa_se.py | 317 --------------------- custom_components/ams/tests/parser_test.py | 18 +- custom_components/ams/translations/en.json | 11 +- custom_components/ams/translations/nb.json | 11 +- custom_components/ams/translations/nn.json | 11 +- custom_components/ams/translations/no.json | 11 +- custom_components/ams/translations/se.json | 11 +- 13 files changed, 208 insertions(+), 393 deletions(-) delete mode 100644 custom_components/ams/parsers/kaifa_se.py diff --git a/README.md b/README.md index 7f464a0..16e811f 100644 --- a/README.md +++ b/README.md @@ -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) Buy Me A Coffee Custom component reading [AMS](https://no.wikipedia.org/wiki/Smart_str%C3%B8mm%C3%A5ler) @@ -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 +```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' diff --git a/custom_components/ams/__init__.py b/custom_components/ams/__init__.py index a41f472..638c7aa 100755 --- a/custom_components/ams/__init__.py +++ b/custom_components/ams/__init__.py @@ -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, @@ -24,6 +23,7 @@ CONF_METER_MANUFACTURER, CONF_PARITY, CONF_PROTOCOL, + CONF_PROTOCOL_TYPE, CONF_SERIAL_PORT, CONF_TCP_HOST, CONF_TCP_PORT, @@ -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, } ) }, @@ -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 @@ -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: @@ -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 = [] @@ -242,6 +257,7 @@ 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": @@ -249,7 +265,8 @@ def connect(self): # pylint: disable=too-many-branches 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 @@ -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) @@ -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 @@ -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) @@ -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 @@ -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: diff --git a/custom_components/ams/config_flow.py b/custom_components/ams/config_flow.py index c4167f5..b02d34c 100755 --- a/custom_components/ams/config_flow.py +++ b/custom_components/ams/config_flow.py @@ -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, @@ -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( { @@ -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( @@ -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( @@ -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, ) diff --git a/custom_components/ams/const.py b/custom_components/ams/const.py index 0718c63..976c9c0 100755 --- a/custom_components/ams/const.py +++ b/custom_components/ams/const.py @@ -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" @@ -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", diff --git a/custom_components/ams/manifest.json b/custom_components/ams/manifest.json index b195075..f26080d 100755 --- a/custom_components/ams/manifest.json +++ b/custom_components/ams/manifest.json @@ -7,6 +7,6 @@ "codeowners": ["@turbokongen"], "requirements": ["pyserial==3.5", "crccheck==1.0"], "config_flow": true, - "version": "1.9.1", + "version": "1.9.4", "iot_class": "local_push" } diff --git a/custom_components/ams/parsers/kaifa.py b/custom_components/ams/parsers/kaifa.py index 8092200..49a7085 100755 --- a/custom_components/ams/parsers/kaifa.py +++ b/custom_components/ams/parsers/kaifa.py @@ -40,7 +40,7 @@ # pylint: disable=too-many-locals, too-many-statements -def parse_data(stored, data): +def parse_data(stored, data, swedish): """Parse the incoming data to dict.""" sensor_data = {} han_data = {} @@ -70,8 +70,11 @@ def parse_data(stored, data): han_data["date_time"] = date_time_str list_type = pkt[32] han_data[HAN_METER_LIST_TYPE] = list_type - if list_type is LIST_TYPE_MINI: - han_data["active_power_p"] = byte_decode(fields=pkt[34:38]) + if list_type is LIST_TYPE_MINI or swedish: + if swedish: + han_data["active_power_p"] = byte_decode(fields=pkt[71:75]) + else: + han_data["active_power_p"] = byte_decode(fields=pkt[34:38]) sensor_data["ams_active_power_import"] = { SENSOR_STATE: han_data["active_power_p"], SENSOR_ATTR: { @@ -80,8 +83,9 @@ def parse_data(stored, data): SENSOR_ICON: "mdi:gauge", }, } - stored.update(sensor_data) - return stored, han_data + if not swedish: + stored.update(sensor_data) + return stored, han_data han_data[HAN_LIST_VER_ID] = field_type(fields=pkt[35:42], enc=chr) han_data[HAN_METER_SERIAL] = field_type(fields=pkt[44:60], enc=chr) diff --git a/custom_components/ams/parsers/kaifa_se.py b/custom_components/ams/parsers/kaifa_se.py deleted file mode 100644 index 6ef4c2b..0000000 --- a/custom_components/ams/parsers/kaifa_se.py +++ /dev/null @@ -1,317 +0,0 @@ -""" -Decode for Swedish HAN Kaifa. - -This module will decode the incoming message from Mbus serial. -""" -import logging - -from datetime import datetime -from crccheck.crc import CrcX25 -from custom_components.ams.parsers import byte_decode, field_type -from custom_components.ams.const import ( - ACTIVE_ENERGY_SENSORS, - ATTR_DEVICE_CLASS, - ATTR_STATE_CLASS, - CURRENT_SENSORS, - DATA_FLAG, - DEVICE_CLASS_ENERGY, - FRAME_FLAG, - HAN_LIST_VER_ID, - HAN_METER_DATETIME, - HAN_METER_LIST_TYPE, - HAN_METER_MANUFACTURER, - HAN_METER_SERIAL, - HAN_METER_TYPE, - HAN_OBIS_CODE, - HAN_OBIS_DATETIME, - HAN_PACKET_SIZE, - VOLTAGE_SENSORS, - HOURLY_SENSORS, - METER_TYPE, - SENSOR_ATTR, - SENSOR_COMMON_OBIS_MAP, - SENSOR_ICON, - SENSOR_ICON_MAP, - SENSOR_OBIS_MAP, - SENSOR_STATE, - SENSOR_UNIT, - SENSOR_UOM, - STATE_CLASS_TOTAL_INCREASING, - UNKNOWN_METER, - WEEKDAY_MAPPING, -) - -_LOGGER = logging.getLogger(__name__) - - -# pylint: disable=too-many-branches,too-many-locals,too-many-nested-blocks -# pylint: disable=too-many-statements -def parse_data(stored, data): - """Parse the incoming data to dict""" - sensor_data = {} - han_data = {} - pkt = data - read_packet_size = ((data[1] & 0x0F) << 8 | data[2]) + 2 - han_data[HAN_PACKET_SIZE] = read_packet_size - list_type = pkt[19] - han_data[HAN_METER_LIST_TYPE] = list_type - _LOGGER.debug("list_type is %s", list_type) - han_data[HAN_METER_SERIAL] = "00" - han_data[HAN_METER_TYPE] = ( - METER_TYPE.get(field_type(fields=pkt[73:80], enc=chr), UNKNOWN_METER) - ) - han_data[HAN_METER_SERIAL] = field_type(fields=pkt[47:63], enc=chr) - han_data[HAN_LIST_VER_ID] = field_type(fields=pkt[30:37], enc=chr) - - # Get the date and time - for item in SENSOR_COMMON_OBIS_MAP[HAN_METER_DATETIME]: - for i in range(len(pkt)): - if pkt[i:i + len(item)] == item: - # Date time construct - if pkt[i + len(item)] == 9: - han_data[HAN_OBIS_DATETIME] = ( - '.'.join([str(elem) for elem in item]) - ) - v_start = i + len(item) + 2 - meter_date_time_year = ( - byte_decode(fields=pkt[v_start:(v_start + 2)], - count=2)) - meter_date_time_month = pkt[v_start + 2] - meter_date_time_date = pkt[v_start + 3] - meter_date_time_day_of_week = ( - WEEKDAY_MAPPING.get(pkt[v_start + 4])) - meter_date_time_hour = str(pkt[v_start + 5]).zfill(2) - meter_date_time_minute = str(pkt[v_start + 6]).zfill(2) - meter_date_time_seconds = str(pkt[v_start + 7]).zfill(2) - meter_date_time_str = ( - str(meter_date_time_year) - + "-" - + str(meter_date_time_month) - + "-" - + str(meter_date_time_date) - + "-" - + str(meter_date_time_hour) - + "-" - + str(meter_date_time_minute) - + "-" - + str(meter_date_time_minute) - + "-" - + str(meter_date_time_seconds) - ) - han_data[HAN_METER_DATETIME] = meter_date_time_str - _LOGGER.debug("%s, OBIS:%s, Index:%s, Type:%s Double OBIS", - HAN_METER_DATETIME, item, - (i, i + len(item)), (pkt[(i + len(item))])) - _LOGGER.debug("%s, %s, %s, %s, %s, %s, %s, %s, " - "%s, %s", - HAN_METER_DATETIME, - item, meter_date_time_year, - meter_date_time_month, - meter_date_time_date, - meter_date_time_day_of_week, - meter_date_time_hour, - meter_date_time_minute, - meter_date_time_seconds, - meter_date_time_str) - - for key in SENSOR_OBIS_MAP: - if len(SENSOR_OBIS_MAP[key]) == 2: - for item in SENSOR_OBIS_MAP[key]: - for i in range(len(pkt)): - if pkt[i:i + len(item)] == item: - # Double-long-unsigned dict construct - if pkt[i + len(item)] == 6: - v_start = i + len(item) + 1 - v_stop = v_start + 4 - han_data["obis_" + key] = ( - '.'.join([str(elem) for elem in item]) - ) - measure = byte_decode(fields=pkt[v_start:v_stop]) - if key in HOURLY_SENSORS: - han_data[key] = measure / 1000 - elif key in CURRENT_SENSORS: - han_data[key] = measure / 1000 - elif key in VOLTAGE_SENSORS: - han_data[key] = measure / 10 - else: - han_data[key] = measure - sensor_data[key] = { - SENSOR_STATE: han_data[key], - SENSOR_ATTR: { - HAN_METER_MANUFACTURER: han_data[ - HAN_LIST_VER_ID], - HAN_METER_TYPE: han_data[ - HAN_METER_TYPE], - HAN_OBIS_CODE: han_data[ - "obis_" + key], - HAN_METER_SERIAL: han_data[ - HAN_METER_SERIAL], - SENSOR_UOM: - SENSOR_UNIT.get(key), - SENSOR_ICON: ( - "mdi:" + - SENSOR_ICON_MAP.get(key)), - }, - } - if key in HOURLY_SENSORS: - sensor_data[key][SENSOR_ATTR][ - HAN_METER_DATETIME] = han_data[ - HAN_METER_DATETIME] - sensor_data[key][SENSOR_ATTR][ - ATTR_DEVICE_CLASS] = ( - DEVICE_CLASS_ENERGY) - if key in ACTIVE_ENERGY_SENSORS: - sensor_data[key][SENSOR_ATTR][ - ATTR_STATE_CLASS] = ( - STATE_CLASS_TOTAL_INCREASING) - _LOGGER.debug( - "%s, OBIS:%s, Index:%s, Type:%s Double OBIS", - key, item, (i, i + len(item)), - (pkt[(i + len(item))]) - ) - _LOGGER.debug( - "Value double OBIS type 6: %s, Index:%s", - han_data[key], (v_start, v_stop) - ) - # Long-signed & Long-unsigned dict construct - elif (pkt[i + len(item)] == 16 or - pkt[i + len(item)] == 18): - v_start = i + len(item) + 1 - v_stop = v_start + 2 - han_data["obis_" + key] = ( - '.'.join([str(elem) for elem in item]) - ) - han_data[key] = ( - (byte_decode(fields=pkt[v_start:v_stop], - count=2) / 10) - ) - sensor_data[key] = { - SENSOR_STATE: han_data[key], - SENSOR_ATTR: { - HAN_METER_MANUFACTURER: han_data[ - HAN_LIST_VER_ID], - HAN_METER_TYPE: han_data[ - HAN_METER_TYPE], - HAN_OBIS_CODE: han_data[ - "obis_" + key], - HAN_METER_SERIAL: han_data[ - HAN_METER_SERIAL], - SENSOR_UOM: - SENSOR_UNIT.get(key), - SENSOR_ICON: ( - "mdi:" + - SENSOR_ICON_MAP.get(key)), - }, - - } - _LOGGER.debug( - "%s, OBIS:%s, Index:%s, Type:%s Double OBIS", - key, item, (i, i + len(item)), - (pkt[(i + len(item))])) - _LOGGER.debug( - "Value double OBIS type 16/18: %s, Index:%s", - han_data[key], (v_start, v_stop)) - # Visible string construct - elif pkt[i + len(item)] == 10: - v_start = i + len(item) + 2 - v_length = pkt[v_start - 1] - v_stop = v_start + v_length - _LOGGER.debug( - "%s, OBIS:%s, Index:%s, Type:%s Double OBIS", - key, item, (i, i + len(item)), - (pkt[(i + len(item))])) - _LOGGER.debug( - "Value double OBIS type 10: %s, Index:%s", - han_data[key], (v_start, v_stop)) - - for i in range(len(pkt)): - if (pkt[i:i + len(SENSOR_OBIS_MAP[key])] == - SENSOR_OBIS_MAP[key]): - # Double-long-unsigned construct - if pkt[i + len(SENSOR_OBIS_MAP[key])] == 6: - v_start = i + len(SENSOR_OBIS_MAP[key]) + 1 - v_stop = v_start + 4 - han_data["obis_" + key] = ( - '.'.join([str(elem) for elem in - SENSOR_OBIS_MAP[key]]) - ) - han_data[key] = byte_decode(fields=pkt[v_start:v_stop]) - sensor_data[key] = { - SENSOR_STATE: han_data[key], - SENSOR_ATTR: { - HAN_METER_MANUFACTURER: han_data[ - HAN_LIST_VER_ID], - HAN_METER_TYPE: han_data[ - HAN_METER_TYPE], - HAN_OBIS_CODE: han_data["obis_" + key], - HAN_METER_SERIAL: han_data[ - HAN_METER_SERIAL], - SENSOR_UOM: SENSOR_UNIT.get(key), - SENSOR_ICON: ( - "mdi:" + SENSOR_ICON_MAP.get(key)), - }, - - } - _LOGGER.debug( - "%s, OBIS:%s, Index:%s, Type:%s Single OBIS", key, - SENSOR_OBIS_MAP[key], (i, i + len( - SENSOR_OBIS_MAP[key])), - (pkt[(i + len(SENSOR_OBIS_MAP[key]))])) - _LOGGER.debug( - "Value single OBIS type 6: %s Index:%s", - han_data[key], (v_start, v_stop)) - - stored.update(sensor_data) - return stored, han_data - - -def test_valid_data(data): - """"Test the incoming data for validity.""" - # pylint: disable=too-many-return-statements - if data is None: - return False - - if len(data) > 581 or len(data) < 44: - _LOGGER.debug("Invalid packet size %s", len(data)) - return False - - if not data[0] and data[-1] == FRAME_FLAG: - _LOGGER.debug( - "%s Received %s bytes of %s data", - datetime.now().isoformat(), - len(data), - False, - ) - return False - - header_checksum = CrcX25.calc(bytes(data[1:7])) - read_header_checksum = data[8] << 8 | data[7] - - if header_checksum != read_header_checksum: - _LOGGER.debug("Invalid header CRC check") - return False - - frame_checksum = CrcX25.calc(bytes(data[1:-3])) - read_frame_checksum = data[-2] << 8 | data[-3] - - if frame_checksum != read_frame_checksum: - _LOGGER.debug("Invalid frame CRC check") - return False - - if data[9:13] != DATA_FLAG: - _LOGGER.debug("Data does not start with %s: %s", DATA_FLAG, - data[9:13]) - return False - - packet_size = len(data) - read_packet_size = ((data[1] & 0x0F) << 8 | data[2]) + 2 - - if packet_size != read_packet_size: - _LOGGER.debug( - "Packet size does not match read packet size: %s : %s", - packet_size, - read_packet_size, - ) - return False - - return True diff --git a/custom_components/ams/tests/parser_test.py b/custom_components/ams/tests/parser_test.py index e54f447..4d54bc3 100644 --- a/custom_components/ams/tests/parser_test.py +++ b/custom_components/ams/tests/parser_test.py @@ -4,16 +4,20 @@ import pprint import sys from homeassistant.core import HomeAssistant +import homeassistant.helpers.config_validation as cv +import voluptuous as vol +from custom_components.ams.const import * +import custom_components.ams from custom_components.ams.parsers import aidon from custom_components.ams.parsers import aidon_se from custom_components.ams.parsers import kaifa -from custom_components.ams.parsers import kaifa_se from custom_components.ams.parsers import kamstrup from custom_components.ams import AmsHub from custom_components.ams.const import DOMAIN -METERTYPE = kaifa # Input parser to use -PACKAGE = [126, 160, 39, 1, 2, 1, 16, 90, 135, 230, 231, 0, 15, 64, 0, 0, 0, 9, 12, 7, 229, 10, 22, 5, 11, 29, 26, 255, 128, 0, 0, 2, 1, 6, 0, 0, 8, 69, 105, 27, 126] +METERTYPE = kaifa # Input parser to use +SWEDISH = True +PACKAGE = [126, 160, 155, 1, 0, 1, 16, 86, 27, 230, 231, 0, 15, 64, 0, 0, 0, 9, 12, 7, 230, 9, 18, 7, 14, 56, 15, 255, 128, 0, 0, 2, 18, 9, 7, 75, 70, 77, 95, 48, 48, 49, 9, 16, 55, 51, 52, 48, 49, 53, 55, 48, 49, 49, 50, 55, 52, 53, 51, 50, 9, 8, 77, 65, 51, 48, 52, 72, 52, 68, 6, 0, 0, 6, 54, 6, 0, 0, 0, 0, 6, 0, 0, 0, 0, 6, 0, 0, 1, 208, 6, 0, 0, 2, 212, 6, 0, 0, 16, 217, 6, 0, 0, 9, 187, 6, 0, 0, 8, 235, 6, 0, 0, 9, 2, 6, 0, 0, 8, 251, 9, 12, 7, 230, 9, 18, 7, 14, 56, 15, 255, 128, 0, 0, 6, 8, 166, 101, 185, 6, 0, 0, 0, 0, 6, 2, 217, 43, 105, 6, 0, 34, 14, 106, 197, 201, 126] PKG = [] for item in PACKAGE: PKG.append(hex(item)[2:].zfill(2)) @@ -49,17 +53,19 @@ if meter_validData: print("--------------Valid data test passed----------------") print("Running parse_data.......................") -meter_data, _ = METERTYPE.parse_data(sensor_data, PACKAGE) +meter_data, _ = METERTYPE.parse_data(sensor_data, PACKAGE, SWEDISH) print("Checking for missing attributes") print(type(meter_data)) -Config = { +config = { + "protocol": "", "serial_port": "/dev/serial/by-id/usb-Prolific_Technology_Inc._USB" "-Serial_Controller-if00-port0", "meter_manufacturer": "auto", "parity": "N", "baudrate": 2400 } -hub = AmsHub(HomeAssistant, Config) + +hub = AmsHub(HomeAssistant, config) hass = HomeAssistant hass.data = {} hass.data[DOMAIN] = hub diff --git a/custom_components/ams/translations/en.json b/custom_components/ams/translations/en.json index bf83d5a..05ebe0a 100755 --- a/custom_components/ams/translations/en.json +++ b/custom_components/ams/translations/en.json @@ -21,7 +21,16 @@ "meter_manufacturer": "Meter manufacturer" } }, - "serial_connection": { + "select_serial_connection": { + "description": "Setup options for serial communication", + "data": { + "serial_port": "Serial Port", + "parity": "Parity", + "meter_manufacturer": "Meter manufacturer", + "baudrate": "Baud rate" + } + }, + "enter_serial_connection": { "description": "Setup options for serial communication", "data": { "serial_port": "Serial Port", diff --git a/custom_components/ams/translations/nb.json b/custom_components/ams/translations/nb.json index 22fae19..25dd0be 100755 --- a/custom_components/ams/translations/nb.json +++ b/custom_components/ams/translations/nb.json @@ -20,7 +20,7 @@ "meter_manufacturer": "Måler produsent" } }, - "serial_connection": { + "select_serial_connection": { "description": "Velg portdata for serietilkobling", "data": { "serial_port": "Serieport", @@ -28,6 +28,15 @@ "meter_manufacturer": "Måler produsent", "baudrate": "Baudrate" } + }, + "enter_serial_connection": { + "description": "Setup options for serial communication", + "data": { + "serial_port": "Serial Port", + "parity": "Parity", + "meter_manufacturer": "Meter manufacturer", + "baudrate": "Baud rate" + } } }, "error": { diff --git a/custom_components/ams/translations/nn.json b/custom_components/ams/translations/nn.json index a37c771..a14160b 100755 --- a/custom_components/ams/translations/nn.json +++ b/custom_components/ams/translations/nn.json @@ -22,7 +22,7 @@ "meter_manufacturer": "Målar produsent" } }, - "serial_connection": { + "select_serial_connection": { "title": "AMS Noreg", "description": "Vel portdata for serietilkobling", "data": { @@ -31,6 +31,15 @@ "meter_manufacturer": "Målar produsent", "baudrate": "Baudrate" } + }, + "enter_serial_connection": { + "description": "Vel portdata for serietilkobling", + "data": { + "serial_port": "Serieport", + "parity": "Paritet", + "meter_manufacturer": "Målar produsent", + "baudrate": "Baudrate" + } } }, "error": { diff --git a/custom_components/ams/translations/no.json b/custom_components/ams/translations/no.json index 9e2ef6d..7a039ce 100755 --- a/custom_components/ams/translations/no.json +++ b/custom_components/ams/translations/no.json @@ -21,7 +21,16 @@ "meter_manufacturer": "Måler produsent" } }, - "serial_connection": { + "select_serial_connection": { + "description": "Velg portdata for serietilkobling", + "data": { + "serial_port": "Serieport", + "parity": "Paritet", + "meter_manufacturer": "Måler produsent", + "baudrate": "Baudrate" + } + }, + "enter_serial_connection": { "description": "Velg portdata for serietilkobling", "data": { "serial_port": "Serieport", diff --git a/custom_components/ams/translations/se.json b/custom_components/ams/translations/se.json index 99007bb..78d75ce 100644 --- a/custom_components/ams/translations/se.json +++ b/custom_components/ams/translations/se.json @@ -21,7 +21,7 @@ "meter_manufacturer": "Mätartilverkare" } }, - "serial_connection": { + "select_serial_connection": { "description": "Välj data for seriell anslutning", "data": { "serial_port": "Serieport", @@ -29,6 +29,15 @@ "meter_manufacturer": "Mätartilverkare", "baudrate": "Baudrate" } + }, + "enter_manual_serial_connection": { + "description": "Vãlj data for seriell anslutning", + "data": { + "serial_port": "Serieport", + "parity": "Paritet", + "meter_manufacturer": "Mãtartilverkare", + "baudrate": "Baud rate" + } } }, "error": {