def main(url): handler = SmlEvent() proto = SmlProtocol(url) proto.add_listener(handler.event, ['SmlGetListResponse']) loop = asyncio.get_event_loop() loop.run_until_complete(proto.connect(loop)) loop.run_forever()
class SmlMqttBridge(MqttBridge): HASS_SENSORS = { # 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=2: Active power - # D=8: Time integral 1 # E=0: Total "1-0:2.8.0*255": "Negative active energy total", # E=1: Rate 1 "1-0:2.8.1*255": "Negative active energy in tariff T1", # E=2: Rate 2 "1-0:2.8.2*255": "Negative active energy in tariff T2", # 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", # C=31: Active amperage L1 # D=7: Instantaneous value # E=0: Total "1-0:31.7.0*255": "L1 active instantaneous amperage", # C=36: Active power L1 # D=7: Instantaneous value # E=0: Total "1-0:36.7.0*255": "L1 active instantaneous power", # C=51: Active amperage L2 # D=7: Instantaneous value # E=0: Total "1-0:51.7.0*255": "L2 active instantaneous amperage", # C=56: Active power L2 # D=7: Instantaneous value # E=0: Total "1-0:56.7.0*255": "L2 active instantaneous power", # C=71: Active amperage L3 # D=7: Instantaneous value # E=0: Total "1-0:71.7.0*255": "L3 active instantaneous amperage", # C=76: Active power L3 # D=7: Instantaneous value # E=0: Total "1-0:76.7.0*255": "L3 active instantaneous power", # C=96: Electricity-related service entries "1-0:96.1.0*255": "Metering point ID 1", } def __init__(self, cfg: dict): super().__init__(cfg) self._cache = {} self._cfg = cfg self._name = "SML" self._proto = SmlProtocol(cfg["port"]) self._wdt = WatchdogTimer(cfg["watchdog_timeout"]) async def _device_id(self) -> str: if not self._cache: await self._wdt.wait_reset() # Serial number or public key for obis in ( "1-0:0.0.9*255", "1-0:96.1.0*255", "129-129:199.130.5*255", ): value = self._cache.get(obis, {}).get("value") if value: return value assert False, "No device ID found" async def _base_topic(self, name): device_id = await self._device_id() return f"{self._name}/{slugify(device_id)}/{slugify(name)}" async def _obis_topic(self, objName, attribute): return await self._base_topic(objName) + f"/{attribute}" def _hass_topic(self, component: str, object_id: str) -> str: object_id = slugify(object_id) return f"homeassistant/{component}/sml/{object_id}/config" def _event(self, mqtt, message_body: SmlSequence) -> None: assert isinstance(message_body, SmlGetListResponse) for val in message_body.get("valList", []): name = val.get("objName") if name: logger.debug("OBIS: %s", val) self._cache[name] = val if mqtt: asyncio.create_task(self._publish_obis(mqtt, val)) self._wdt.reset() async def _publish_obis(self, mqtt, obj): name = obj["objName"] for k, v in obj.items(): topic = await self._obis_topic(name, k) await self._publish(mqtt, topic, v) async def _publish_availability(self, mqtt, status: bool) -> None: await self._publish( mqtt, await self._base_topic("availability"), [b"offline", b"online"][status], retain=True, ) async def _publish_hass_config(self, mqtt) -> None: device_id = await self._device_id() availability_topic = await self._base_topic("availability") device = {"name": self._name, "identifiers": [device_id]} obj = self._cache.get("129-129:199.130.3*255") if obj and "value" in obj: device["manufacturer"] = obj["value"] else: parts = device_id.split() if len(parts) == 4 and len(parts[1] == 3): device["manufacturer"] = parts[1] # https://www.home-assistant.io/integrations/sensor.mqtt/ prefix = self._cfg["hass_name"] for obis, name in self.HASS_SENSORS.items(): obj = self._cache.get(obis) if not obj: continue object_id = f"{device_id}-{obis}" config = { "availability_topic": availability_topic, "device": device, "name": prefix and f"{prefix} {name}" or name, "state_topic": await self._obis_topic(obis, "value"), "unique_id": object_id, } unit = obj.get("unit") if unit: config["unit_of_measurement"] = unit if unit == "W": config["device_class"] = "power" elif unit == "Wh": config["device_class"] = "energy" topic = self._hass_topic("sensor", object_id) await self._publish(mqtt, topic, config, retain=True) async def run(self) -> None: event = partial(self._event, None) self._proto.add_listener(event, ["SmlGetListResponse"]) await self._proto.connect() p = urlparse(self._cfg["broker"], scheme="mqtt") if p.scheme not in ("mqtt", "mqtts") or not p.hostname: raise ValueError tls_context = None if p.scheme == "mqtts": tls_context = ssl.create_default_context() will = Will( await self._base_topic("availability"), payload=b"offline", qos=2, retain=True, ) self._proto.remove_listener(event, ["SmlGetListResponse"]) async with Client( p.hostname, port=p.port or p.scheme == "mqtt" and 1883 or 8883, username=p.username, password=p.password, logger=logger, tls_context=tls_context, will=will, ) as mqtt: watchdog_callback = partial(self._publish_availability, mqtt) self._wdt.start(watchdog_callback) event = partial(self._event, mqtt) self._proto.add_listener(event, ["SmlGetListResponse"]) if self._cfg["hass"]: await self._publish_hass_config(mqtt) await self._run_mainloop(mqtt) self._proto.remove_listener(event, ["SmlGetListResponse"]) self._wdt.stop() await self._publish_availability(mqtt, False)
class EDL21: """EDL21 handles telegrams sent by a compatible smart meter.""" _OBIS_BLACKLIST = { # C=96: Electricity-related service entries "1-0:96.50.1*1", # Manufacturer specific "1-0:96.90.2*1", # Manufacturer specific "1-0:96.90.2*2", # Manufacturer specific # 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[tuple[str, str]] = set() self._hass = hass self._async_add_entities = async_add_entities self._name = config[CONF_NAME] 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) electricity_id = None for telegram in message_body.get("valList", []): if telegram.get("objName") in ("1-0:0.0.9*255", "1-0:96.1.0*255"): electricity_id = telegram.get("value") break if electricity_id is None: return electricity_id = electricity_id.replace(" ", "") new_entities = [] for telegram in message_body.get("valList", []): if not (obis := telegram.get("objName")): continue if (electricity_id, obis) in self._registered_obis: async_dispatcher_send(self._hass, SIGNAL_EDL21_TELEGRAM, electricity_id, telegram) else: entity_description = SENSORS.get(obis) if entity_description and entity_description.name: name = entity_description.name if self._name: name = f"{self._name}: {name}" new_entities.append( EDL21Entity(electricity_id, obis, name, entity_description, telegram)) self._registered_obis.add((electricity_id, obis)) elif obis not in self._OBIS_BLACKLIST: _LOGGER.warning( "Unhandled sensor %s detected. Please report at " 'https://github.com/home-assistant/core/issues?q=is%%3Aissue+label%%3A"integration%%3A+edl21"+', obis, ) self._OBIS_BLACKLIST.add(obis) if new_entities: self._hass.loop.create_task(self.add_entities(new_entities))
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=2: Active power - # D=8: Time integral 1 # E=0: Total "1-0:2.8.0*255": "Negative active energy total", # E=1: Rate 1 "1-0:2.8.1*255": "Negative active energy in tariff T1", # E=2: Rate 2 "1-0:2.8.2*255": "Negative active energy in tariff T2", # 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", # C=31: Active amperage L1 # D=7: Instantaneous value # E=0: Total "1-0:31.7.0*255": "L1 active instantaneous amperage", # C=36: Active power L1 # D=7: Instantaneous value # E=0: Total "1-0:36.7.0*255": "L1 active instantaneous power", # C=51: Active amperage L2 # D=7: Instantaneous value # E=0: Total "1-0:51.7.0*255": "L2 active instantaneous amperage", # C=56: Active power L2 # D=7: Instantaneous value # E=0: Total "1-0:56.7.0*255": "L2 active instantaneous power", # C=71: Active amperage L3 # D=7: Instantaneous value # E=0: Total "1-0:71.7.0*255": "L3 active instantaneous amperage", # C=76: Active power L3 # D=7: Instantaneous value # E=0: Total "1-0:76.7.0*255": "L3 active instantaneous power", # C=96: Electricity-related service entries "1-0:96.1.0*255": "Metering point ID 1", } _OBIS_BLACKLIST = { # C=96: Electricity-related service entries "1-0:96.50.1*1", # Manufacturer specific # 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._name = config[CONF_NAME] 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) electricity_id = None for telegram in message_body.get("valList", []): if telegram.get("objName") in ("1-0:0.0.9*255", "1-0:96.1.0*255"): electricity_id = telegram.get("value") break if electricity_id is None: return electricity_id = electricity_id.replace(" ", "") new_entities = [] for telegram in message_body.get("valList", []): obis = telegram.get("objName") if not obis: continue if (electricity_id, obis) in self._registered_obis: async_dispatcher_send(self._hass, SIGNAL_EDL21_TELEGRAM, electricity_id, telegram) else: name = self._OBIS_NAMES.get(obis) if name: if self._name: name = f"{self._name}: {name}" new_entities.append( EDL21Entity(electricity_id, obis, name, telegram)) self._registered_obis.add((electricity_id, obis)) elif obis not in self._OBIS_BLACKLIST: _LOGGER.warning( "Unhandled sensor %s detected. Please report at " 'https://github.com/home-assistant/core/issues?q=is%%3Aissue+label%%3A"integration%%3A+edl21"+', obis, ) self._OBIS_BLACKLIST.add(obis) if new_entities: self._hass.loop.create_task(self.add_entities(new_entities)) async def add_entities(self, new_entities) -> None: """Migrate old unique IDs, then add entities to hass.""" registry = await async_get_registry(self._hass) for entity in new_entities: old_entity_id = registry.async_get_entity_id( "sensor", DOMAIN, entity.old_unique_id) if old_entity_id is not None: _LOGGER.debug( "Migrating unique_id from [%s] to [%s]", entity.old_unique_id, entity.unique_id, ) if registry.async_get_entity_id("sensor", DOMAIN, entity.unique_id): registry.async_remove(old_entity_id) else: registry.async_update_entity( old_entity_id, new_unique_id=entity.unique_id) self._async_add_entities(new_entities, update_before_add=True)
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 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 # D=0: Free ID-numbers for utilities "1-0:0.0.9*255": "Electricity ID", # D=2: Program entries "1-0:0.2.0*0": "Configuration program version number", "1-0:0.2.0*1": "Firmware version number", # 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=2: Active power - # D=8: Time integral 1 # E=0: Total "1-0:2.8.0*255": "Negative active energy total", # E=1: Rate 1 "1-0:2.8.1*255": "Negative active energy in tariff T1", # E=2: Rate 2 "1-0:2.8.2*255": "Negative active energy in tariff T2", # C=14: Supply frequency # D=7: Instantaneous value # E=0: Total "1-0:14.7.0*255": "Supply frequency", # 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", # C=31: Active amperage L1 # D=7: Instantaneous value # E=0: Total "1-0:31.7.0*255": "L1 active instantaneous amperage", # C=32: Active voltage L1 # D=7: Instantaneous value # E=0: Total "1-0:32.7.0*255": "L1 active instantaneous voltage", # C=36: Active power L1 # D=7: Instantaneous value # E=0: Total "1-0:36.7.0*255": "L1 active instantaneous power", # C=51: Active amperage L2 # D=7: Instantaneous value # E=0: Total "1-0:51.7.0*255": "L2 active instantaneous amperage", # C=52: Active voltage L2 # D=7: Instantaneous value # E=0: Total "1-0:52.7.0*255": "L2 active instantaneous voltage", # C=56: Active power L2 # D=7: Instantaneous value # E=0: Total "1-0:56.7.0*255": "L2 active instantaneous power", # C=71: Active amperage L3 # D=7: Instantaneous value # E=0: Total "1-0:71.7.0*255": "L3 active instantaneous amperage", # C=72: Active voltage L3 # D=7: Instantaneous value # E=0: Total "1-0:72.7.0*255": "L3 active instantaneous voltage", # C=76: Active power L3 # D=7: Instantaneous value # E=0: Total "1-0:76.7.0*255": "L3 active instantaneous power", # C=81: Angles # D=7: Instantaneous value # E=4: U(L1) x I(L1) # E=15: U(L2) x I(L2) # E=26: U(L3) x I(L3) "1-0:81.7.4*255": "U(L1)/I(L1) phase angle", "1-0:81.7.15*255": "U(L2)/I(L2) phase angle", "1-0:81.7.26*255": "U(L3)/I(L3) phase angle", # C=96: Electricity-related service entries "1-0:96.1.0*255": "Metering point ID 1", "1-0:96.5.0*255": "Internal operating status", } _OBIS_BLACKLIST = { # C=96: Electricity-related service entries "1-0:96.50.1*1", # Manufacturer specific "1-0:96.90.2*1", # Manufacturer specific "1-0:96.90.2*2", # Manufacturer specific # 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[tuple[str, str]] = set() self._hass = hass self._async_add_entities = async_add_entities self._name = config[CONF_NAME] 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) electricity_id = None for telegram in message_body.get("valList", []): if telegram.get("objName") in ("1-0:0.0.9*255", "1-0:96.1.0*255"): electricity_id = telegram.get("value") break if electricity_id is None: return electricity_id = electricity_id.replace(" ", "") new_entities = [] for telegram in message_body.get("valList", []): if not (obis := telegram.get("objName")): continue if (electricity_id, obis) in self._registered_obis: async_dispatcher_send(self._hass, SIGNAL_EDL21_TELEGRAM, electricity_id, telegram) else: if name := self._OBIS_NAMES.get(obis): if self._name: name = f"{self._name}: {name}" new_entities.append( EDL21Entity(electricity_id, obis, name, telegram)) self._registered_obis.add((electricity_id, obis)) elif obis not in self._OBIS_BLACKLIST: _LOGGER.warning( "Unhandled sensor %s detected. Please report at " 'https://github.com/home-assistant/core/issues?q=is%%3Aissue+label%%3A"integration%%3A+edl21"+', obis, ) self._OBIS_BLACKLIST.add(obis)