Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -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/*
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ homeassistant/components/doorbird/* @oblogic7
homeassistant/components/dweet/* @fabaff
homeassistant/components/ecobee/* @marthoc
homeassistant/components/ecovacs/* @OverloadUT
homeassistant/components/edl21/* @mtdcr
homeassistant/components/egardia/* @jeroenterheerdt
homeassistant/components/eight_sleep/* @mezz64
homeassistant/components/elv/* @majuss
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/edl21/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""The edl21 component."""
12 changes: 12 additions & 0 deletions homeassistant/components/edl21/manifest.json
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"
]
}
196 changes: 196 additions & 0 deletions homeassistant/components/edl21/sensor.py
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 = {
Comment thread
mtdcr marked this conversation as resolved.
# 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",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What kind of time is this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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):
Comment thread
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
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1483,6 +1483,9 @@ pysmartthings==0.6.9
# homeassistant.components.smarty
pysmarty==0.8

# homeassistant.components.edl21
pysml==0.0.2

# homeassistant.components.snmp
pysnmp==4.4.12

Expand Down