Add edl21 component for SML-based smart meters (#27962)

* Add edl21 component for SML-based smart meters

* edl21: Remove unused variable

* [edl21] Add 1 minute throttle to the sensor

* Update homeassistant/components/edl21/manifest.json

Fix documentation URL

Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io>

* edl21: Move imports to top

* edl21: Remove special case for STATE_UNKNOWN, which replicated default behavior

* edl21: Implement blacklist for and warn about unhandled OBIS values

* edl21: Make blacklist global

* edl21: Add filter to issues URL

Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io>

* edl21: Rename device to entity

* edl21: Don't schedule async_add_entities

* edl21: Use dispatcher, implement own throttling mechanism

* edl21: Simplify keeping track of known obis

* edl21: Use whitelist for state attributes

* edl21: Remove dispatcher on shutdown

* edl21: Convert state attributes to snakecase

* edl21: Annotate handle_telegram with @callback

* edl21: Call async_write_ha_state instead of schedule_update_ha_state

Co-authored-by: David Straub <straub@protonmail.com>
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
Paulus Schoutsen 2020-03-04 17:43:12 -08:00 committed by GitHub
parent c4ed2ecb61
commit 2316f7ace4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 214 additions and 0 deletions

View file

@ -180,6 +180,7 @@ omit =
homeassistant/components/ecobee/weather.py
homeassistant/components/econet/*
homeassistant/components/ecovacs/*
homeassistant/components/edl21/*
homeassistant/components/eddystone_temperature/sensor.py
homeassistant/components/edimax/switch.py
homeassistant/components/egardia/*

View file

@ -90,6 +90,7 @@ homeassistant/components/dynalite/* @ziv1234
homeassistant/components/dyson/* @etheralm
homeassistant/components/ecobee/* @marthoc
homeassistant/components/ecovacs/* @OverloadUT
homeassistant/components/edl21/* @mtdcr
homeassistant/components/egardia/* @jeroenterheerdt
homeassistant/components/eight_sleep/* @mezz64
homeassistant/components/elgato/* @frenck

View file

@ -0,0 +1 @@
"""The edl21 component."""

View file

@ -0,0 +1,12 @@
{
"domain": "edl21",
"name": "EDL21",
"documentation": "https://www.home-assistant.io/integrations/edl21",
"requirements": [
"pysml==0.0.2"
],
"dependencies": [],
"codeowners": [
"@mtdcr"
]
}

View file

@ -0,0 +1,196 @@
"""Support for EDL21 Smart Meters."""
from datetime import timedelta
import logging
from sml import SmlGetListResponse
from sml.asyncio import SmlProtocol
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import Optional
from homeassistant.util.dt import utcnow
_LOGGER = logging.getLogger(__name__)
DOMAIN = "edl21"
CONF_SERIAL_PORT = "serial_port"
ICON_POWER = "mdi:flash"
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
SIGNAL_EDL21_TELEGRAM = "edl21_telegram"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_SERIAL_PORT): cv.string})
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the EDL21 sensor."""
hass.data[DOMAIN] = EDL21(hass, config, async_add_entities)
await hass.data[DOMAIN].connect()
class EDL21:
"""EDL21 handles telegrams sent by a compatible smart meter."""
# OBIS format: A-B:C.D.E*F
_OBIS_NAMES = {
# A=1: Electricity
# C=0: General purpose objects
"1-0:0.0.9*255": "Electricity ID",
# C=1: Active power +
# D=8: Time integral 1
# E=0: Total
"1-0:1.8.0*255": "Positive active energy total",
# E=1: Rate 1
"1-0:1.8.1*255": "Positive active energy in tariff T1",
# E=2: Rate 2
"1-0:1.8.2*255": "Positive active energy in tariff T2",
# D=17: Time integral 7
# E=0: Total
"1-0:1.17.0*255": "Last signed positive active energy total",
# C=15: Active power absolute
# D=7: Instantaneous value
# E=0: Total
"1-0:15.7.0*255": "Absolute active instantaneous power",
# C=16: Active power sum
# D=7: Instantaneous value
# E=0: Total
"1-0:16.7.0*255": "Sum active instantaneous power",
}
_OBIS_BLACKLIST = {
# A=129: Manufacturer specific
"129-129:199.130.3*255", # Iskraemeco: Manufacturer
"129-129:199.130.5*255", # Iskraemeco: Public Key
}
def __init__(self, hass, config, async_add_entities) -> None:
"""Initialize an EDL21 object."""
self._registered_obis = set()
self._hass = hass
self._async_add_entities = async_add_entities
self._proto = SmlProtocol(config[CONF_SERIAL_PORT])
self._proto.add_listener(self.event, ["SmlGetListResponse"])
async def connect(self):
"""Connect to an EDL21 reader."""
await self._proto.connect(self._hass.loop)
def event(self, message_body) -> None:
"""Handle events from pysml."""
assert isinstance(message_body, SmlGetListResponse)
new_entities = []
for telegram in message_body.get("valList", []):
obis = telegram.get("objName")
if not obis:
continue
if obis in self._registered_obis:
async_dispatcher_send(self._hass, SIGNAL_EDL21_TELEGRAM, telegram)
else:
name = self._OBIS_NAMES.get(obis)
if name:
new_entities.append(EDL21Entity(obis, name, telegram))
self._registered_obis.add(obis)
elif obis not in self._OBIS_BLACKLIST:
_LOGGER.warning(
"Unhandled sensor %s detected. Please report at "
'https://github.com/home-assistant/home-assistant/issues?q=is%%3Aissue+label%%3A"integration%%3A+edl21"+',
obis,
)
self._OBIS_BLACKLIST.add(obis)
if new_entities:
self._async_add_entities(new_entities, update_before_add=True)
class EDL21Entity(Entity):
"""Entity reading values from EDL21 telegram."""
def __init__(self, obis, name, telegram):
"""Initialize an EDL21Entity."""
self._obis = obis
self._name = name
self._telegram = telegram
self._min_time = MIN_TIME_BETWEEN_UPDATES
self._last_update = utcnow()
self._state_attrs = {
"status": "status",
"valTime": "val_time",
"scaler": "scaler",
"valueSignature": "value_signature",
}
self._async_remove_dispatcher = None
async def async_added_to_hass(self):
"""Run when entity about to be added to hass."""
@callback
def handle_telegram(telegram):
"""Update attributes from last received telegram for this object."""
if self._obis != telegram.get("objName"):
return
if self._telegram == telegram:
return
now = utcnow()
if now - self._last_update < self._min_time:
return
self._telegram = telegram
self._last_update = now
self.async_write_ha_state()
self._async_remove_dispatcher = async_dispatcher_connect(
self.hass, SIGNAL_EDL21_TELEGRAM, handle_telegram
)
async def async_will_remove_from_hass(self):
"""Run when entity will be removed from hass."""
if self._async_remove_dispatcher:
self._async_remove_dispatcher()
@property
def should_poll(self) -> bool:
"""Do not poll."""
return False
@property
def unique_id(self) -> str:
"""Return a unique ID."""
return self._obis
@property
def name(self) -> Optional[str]:
"""Return a name."""
return self._name
@property
def state(self) -> str:
"""Return the value of the last received telegram."""
return self._telegram.get("value")
@property
def device_state_attributes(self):
"""Enumerate supported attributes."""
return {
self._state_attrs[k]: v
for k, v in self._telegram.items()
if k in self._state_attrs
}
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._telegram.get("unit")
@property
def icon(self):
"""Return an icon."""
return ICON_POWER

View file

@ -1529,6 +1529,9 @@ pysmartthings==0.7.0
# homeassistant.components.smarty
pysmarty==0.8
# homeassistant.components.edl21
pysml==0.0.2
# homeassistant.components.snmp
pysnmp==4.4.12