예제 #1
0
파일: test_asyncio.py 프로젝트: mtdcr/pysml
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()
예제 #2
0
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)
예제 #3
0
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))
예제 #4
0
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)
예제 #5
0
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)
예제 #6
0
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)