-
-
Notifications
You must be signed in to change notification settings - Fork 37.5k
Add edl21 component for SML-based smart meters #27962
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7236e08
d91b687
5a13763
d117a17
035fbd0
b7e219f
5202313
1561b9e
ce7976a
fe24c73
77e3642
9f9786d
1309988
152b485
2dbbb4f
4840a27
45a0022
4221c3f
feb2a67
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| """The edl21 component.""" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| { | ||
| "domain": "edl21", | ||
| "name": "EDL21", | ||
| "documentation": "https://www.home-assistant.io/integrations/edl21", | ||
| "requirements": [ | ||
| "pysml==0.0.2" | ||
| ], | ||
| "dependencies": [], | ||
| "codeowners": [ | ||
| "@mtdcr" | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What kind of time is this?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's an optional timestamp which indicates when the measurement was performed. My device doesn't use it, though.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok 👍 Absolute timestamps are ok and they should be UTC. |
||
| "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): | ||
|
mtdcr marked this conversation as resolved.
|
||
| """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 | ||
Uh oh!
There was an error while loading. Please reload this page.