Skip to content

Commit

Permalink
V1.7.2 (#59)
Browse files Browse the repository at this point in the history
* Add support for swedish kaifa meters

* Update readme

* Update readme

* Adjust measurement for active power

* More adjusting to kaifa_se

* Rename pakcage

* Add missing kamstrup meter. Add unknown meter for unidentified meters

* Address Unknown unit for reactive energy sensors. Code quality fixes. Add swedish translation.

* Update manifest.json

* Prevent unknown state class None. Add missing kaifa_se option

* Correct name for kamstrup meter

* Code quality and some documentation
  • Loading branch information
turbokongen authored Oct 15, 2021
1 parent 49db292 commit 454c20d
Show file tree
Hide file tree
Showing 11 changed files with 106 additions and 39 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ Supports the new energy dashboard in Home-Assistant.

If it does not decode your data, please submit a ticket, and I will try to
make a parser for your meter.

If your meter type shown "unknown", please submit a ticket, and I will add
your meter to the module.

Expand Down
47 changes: 28 additions & 19 deletions custom_components/ams/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,9 @@ async def async_setup(hass: HomeAssistant, config: Config):
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up AMS as config entry."""
_setup(hass, entry.data)
hass.async_add_job(hass.config_entries.async_forward_entry_setup(entry,
"sensor"))
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(
entry, "sensor"))
return True


Expand All @@ -103,7 +104,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
return True


async def async_remove_entry(hass, entry):
async def async_remove_entry(hass, entry): # pylint: disable=unused-argument
"""Handle removal of an entry."""
await hass.async_add_executor_job(hass.data[DOMAIN].stop_serial_read)
return True
Expand Down Expand Up @@ -167,15 +168,17 @@ def meter_type(self):

return self._attrs[HAN_METER_TYPE]

def connect(self):
def connect(self): # pylint: disable=too-many-branches
"""Read the data from the port."""
parser = None

detect_pkg = None # This is needed to push the package used for
# detecting the meter straight to the parser. If not, users will get
# unknown state class None for energy sensors at startup.
if self.meter_manufacturer == "auto":
while parser is None:
_LOGGER.info("Autodetecting meter manufacturer")
pkg = self.read_bytes()
self.meter_manufacturer = self._find_parser(pkg)
detect_pkg = self.read_bytes()
self.meter_manufacturer = self._find_parser(detect_pkg)
parser = self.meter_manufacturer

if self.meter_manufacturer == "aidon":
Expand All @@ -191,26 +194,33 @@ def connect(self):

while self._running:
try:
data = self.read_bytes()
if detect_pkg:
data = detect_pkg
else:
data = self.read_bytes()
if parser.test_valid_data(data):
_LOGGER.debug("data read from port=%s", data)
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)
if detect_pkg:
detect_pkg = None
except serial.serialutil.SerialException:
pass

def _find_parser(self, pkg):
@classmethod
def _find_parser(cls, pkg):
"""Helper to detect meter manufacturer."""

def _test_meter(pkg, meter):
def _test_meter(test_pkg, meter):
"""Meter tester."""
match = []
_LOGGER.debug("Testing for %s", meter)
for i in range(len(pkg)):
if pkg[i] == meter[0] and pkg[i:(i + len(meter))] == meter:
for i, _ in enumerate(test_pkg):
if test_pkg[i] == meter[0] and (
test_pkg[i:(i + len(meter))] == meter):
match.append(meter)
return meter in match

Expand All @@ -234,7 +244,7 @@ def _test_meter(pkg, meter):
return "kamstrup"

_LOGGER.warning("No parser detected")
_LOGGER.debug("Package dump: %s", pkg)
_LOGGER.debug("Meter detection package dump: %s", pkg)

@property
def data(self):
Expand All @@ -253,22 +263,21 @@ def missing_attrs(self, data=None):
cp_sensors_data = deepcopy(data)
for check in miss_attrs:
for value in cp_sensors_data.values():
v = value.get(SENSOR_ATTR, {}).get(check)
if v:
self._attrs[check] = v
val = value.get(SENSOR_ATTR, {}).get(check)
if val:
self._attrs[check] = val
break
del cp_sensors_data
return len([i for i in attrs_to_check if i not in self._attrs])
else:
return False
return False

def _check_for_new_sensors_and_update(self, sensor_data):
"""Compare sensor list and update."""
new_devices = []
sensors_in_data = set(sensor_data.keys())
new_devices = sensors_in_data.difference(AMS_DEVICES)

if len(new_devices):
if new_devices:
# Check that we have all the info we need before the sensors are
# created, the most important one is the meter_serial as this is
# use to create the unique_id
Expand Down
4 changes: 2 additions & 2 deletions custom_components/ams/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import voluptuous as vol
from homeassistant import config_entries

from custom_components.ams.const import (
from custom_components.ams.const import ( # pylint: disable=unused-import
CONF_BAUDRATE,
CONF_METER_MANUFACTURER,
CONF_PARITY,
Expand Down Expand Up @@ -79,7 +79,7 @@ async def async_step_import(self, import_config):
"""Import a config flow from configuration."""
if self._async_current_entries():
_LOGGER.warning("Only one configuration of AMS Reader is allowed.")
return self.async_abort(reason="singel_instance_allowed")
return self.async_abort(reason="single_instance_allowed")

return self.async_create_entry(title="configuration.yaml",
data=import_config)
Expand Down
21 changes: 19 additions & 2 deletions custom_components/ams/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@
6841131: "Omnipower 3 Phase 4-Wire Direct meter",
6851121: "Omnipower 3 Phase CT 3-Wire Direct meter",
6851131: "Omnipower 3 Phase CT 4-Wire Direct meter",
6851128: "Omnipower 3 Phase Direct meter",
6841128: "Omnipower 3 Phase Direct meter",
6841138: "Omnipower 3 Phase Direct meter",
}
UNKNOWN_METER = "Unknown"
Expand All @@ -130,6 +130,16 @@
HAN_REACTIVE_ENERGY_EXPORT,
]

ACTIVE_ENERGY_SENSORS = [
HAN_ACTIVE_ENERGY_IMPORT,
HAN_ACTIVE_ENERGY_EXPORT,
]

ACTIVE_ENERGY_DEFAULT_ATTRS = {
ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING,
ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
}

CURRENT_SENSORS = [
HAN_CURRENT_L1,
HAN_CURRENT_L2,
Expand All @@ -155,7 +165,14 @@
HAN_CURRENT_L3,
] + HOURLY_SENSORS

MANUFACTURER_OPTIONS = ["auto", "aidon", "aidon_se", "kaifa", "kamstrup"]
MANUFACTURER_OPTIONS = [
"auto",
"aidon",
"aidon_se",
"kaifa",
"kaifa_se,",
"kamstrup",
]

SIGNAL_UPDATE_AMS = "ams_update"
SIGNAL_NEW_AMS_SENSOR = "ams_new_sensor"
Expand Down
2 changes: 1 addition & 1 deletion custom_components/ams/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
"codeowners": ["@turbokongen"],
"requirements": ["pyserial==3.5", "crccheck==1.0"],
"config_flow": true,
"version": "1.7.1",
"version": "1.7.2",
"iot_class": "local_push"
}
8 changes: 5 additions & 3 deletions custom_components/ams/parsers/aidon.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
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,
DATA_FLAG,
Expand Down Expand Up @@ -258,12 +259,13 @@ def parse_data(stored, data):
sensor_data[key][SENSOR_ATTR][
HAN_METER_DATETIME] = han_data[
HAN_METER_DATETIME]
sensor_data[key][SENSOR_ATTR][
ATTR_STATE_CLASS] = (
STATE_CLASS_TOTAL_INCREASING)
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(
"Value double OBIS type 6: %s, Index:%s",
han_data[key], (v_start, v_stop)
Expand Down
8 changes: 5 additions & 3 deletions custom_components/ams/parsers/aidon_se.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from crccheck.crc import CrcX25
from custom_components.ams.parsers import byte_decode
from custom_components.ams.const import (
ACTIVE_ENERGY_SENSORS,
ATTR_DEVICE_CLASS,
ATTR_STATE_CLASS,
DATA_FLAG,
Expand Down Expand Up @@ -150,12 +151,13 @@ def parse_data(stored, data):
sensor_data[key][SENSOR_ATTR][
HAN_METER_DATETIME] = han_data[
HAN_METER_DATETIME]
sensor_data[key][SENSOR_ATTR][
ATTR_STATE_CLASS] = (
STATE_CLASS_TOTAL_INCREASING)
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)),
Expand Down
8 changes: 5 additions & 3 deletions custom_components/ams/parsers/kaifa_se.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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,
Expand Down Expand Up @@ -156,12 +157,13 @@ def parse_data(stored, data):
sensor_data[key][SENSOR_ATTR][
HAN_METER_DATETIME] = han_data[
HAN_METER_DATETIME]
sensor_data[key][SENSOR_ATTR][
ATTR_STATE_CLASS] = (
STATE_CLASS_TOTAL_INCREASING)
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)),
Expand Down
8 changes: 5 additions & 3 deletions custom_components/ams/parsers/kamstrup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from datetime import datetime
from crccheck.crc import CrcX25
from custom_components.ams.const import (
ACTIVE_ENERGY_SENSORS,
ATTR_DEVICE_CLASS,
ATTR_STATE_CLASS,
DATA_FLAG,
Expand Down Expand Up @@ -228,12 +229,13 @@ def parse_data(stored, data):
sensor_data[key][SENSOR_ATTR][
HAN_METER_DATETIME] = han_data[
HAN_METER_DATETIME]
sensor_data[key][SENSOR_ATTR][
ATTR_STATE_CLASS] = (
STATE_CLASS_TOTAL_INCREASING)
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)),
Expand Down
15 changes: 13 additions & 2 deletions custom_components/ams/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from homeassistant.util import dt as dt_utils

from custom_components.ams.const import (
ACTIVE_ENERGY_DEFAULT_ATTRS,
ACTIVE_ENERGY_SENSORS,
AMS_DEVICES,
AMS_SENSOR_CREATED_BUT_NOT_READ,
DOMAIN,
Expand All @@ -21,6 +23,7 @@


async def async_setup_entry(hass, config_entry, async_add_devices):
# pylint: disable=unused-argument
"""Setup sensor platform for the ui"""

@callback
Expand Down Expand Up @@ -59,10 +62,18 @@ def async_add_sensor():
"state": data.get(hourly, {}).get("state"),
"attributes": data.get(hourly, {}).get("attributes"),
}
if hourly in ACTIVE_ENERGY_SENSORS:
sensor_states = {
"name": hourly,
"state": data.get(hourly, {}).get("state"),
"attributes": data.get(hourly, {}).get("attributes", (
ACTIVE_ENERGY_DEFAULT_ATTRS)),
}
sensors.append(AmsSensor(hass, sensor_states))

if len(sensors):
_LOGGER.debug("Trying to add %s sensors", len(sensors))
if sensors:
_LOGGER.debug("Trying to add %s sensors: %s", len(sensors),
sensors)
async_add_devices(sensors)

async_dispatcher_connect(hass, SIGNAL_NEW_AMS_SENSOR, async_add_sensor)
Expand Down
23 changes: 23 additions & 0 deletions custom_components/ams/translations/se.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"config": {
"abort": {
"single_instance_allowed": "Endast en inträde av AMS läsare är tillåten"
},
"title": "AMS Reader",
"step": {
"user": {
"title": "AMS läsare",
"description": "Installera sensorer för AMS läsare",
"data": {
"serial_port": "Serieport",
"parity": "Paritet",
"meter_manufacturer": "Mätar tilverkare",
"baudrate": "Baudrate"
}
}
},
"error": {
"name_exists": "Namn finns"
}
}
}

0 comments on commit 454c20d

Please sign in to comment.