Skip to content

Commit

Permalink
Update DSMR integration to import yaml to ConfigEntry (home-assistant…
Browse files Browse the repository at this point in the history
…#39473)

* Rewrite to import from platform setup

* Add config flow for import

* Implement reload

* Update sensor tests

* Add config flow tests

* Remove some code

* Fix pylint issue

* Remove update options code

* Add platform import test

* Remove infinite while loop

* Move async_setup_platform

* Check for unload_ok

* Remove commented out test code

* Implement function to check on host/port already existing

Co-authored-by: Chris Talkington <chris@talkingtontech.com>

* Implement new method in import

* Update tests

* Fix test setup platform

* Add string

* Patch setup_platform

* Add block till done to patch block

Co-authored-by: Chris Talkington <chris@talkingtontech.com>
  • Loading branch information
RobBie1221 and ctalkington authored Sep 3, 2020
1 parent 77f5fb7 commit d0120d5
Show file tree
Hide file tree
Showing 9 changed files with 498 additions and 68 deletions.
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ homeassistant/components/digital_ocean/* @fabaff
homeassistant/components/directv/* @ctalkington
homeassistant/components/discogs/* @thibmaek
homeassistant/components/doorbird/* @oblogic7 @bdraco
homeassistant/components/dsmr/* @Robbie1221
homeassistant/components/dsmr_reader/* @depl0y
homeassistant/components/dunehd/* @bieniu
homeassistant/components/dwd_weather_warnings/* @runningman84 @stephan192 @Hummel95
Expand Down
53 changes: 53 additions & 0 deletions homeassistant/components/dsmr/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,54 @@
"""The dsmr component."""
import asyncio
from asyncio import CancelledError
import logging

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant

from .const import DATA_TASK, DOMAIN, PLATFORMS

_LOGGER = logging.getLogger(__name__)


async def async_setup(hass, config: dict):
"""Set up the DSMR platform."""
return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up DSMR from a config entry."""
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = {}

for platform in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, platform)
)

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
task = hass.data[DOMAIN][entry.entry_id][DATA_TASK]

# Cancel the reconnect task
task.cancel()
try:
await task
except CancelledError:
pass

unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
)
)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)

return unload_ok
61 changes: 61 additions & 0 deletions homeassistant/components/dsmr/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Config flow for DSMR integration."""
import logging
from typing import Any, Dict, Optional

from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_PORT

from .const import DOMAIN # pylint:disable=unused-import

_LOGGER = logging.getLogger(__name__)


class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for DSMR."""

VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH

def _abort_if_host_port_configured(
self,
port: str,
host: str = None,
updates: Optional[Dict[Any, Any]] = None,
reload_on_update: bool = True,
):
"""Test if host and port are already configured."""
for entry in self.hass.config_entries.async_entries(DOMAIN):
if entry.data.get(CONF_HOST) == host and entry.data[CONF_PORT] == port:
if updates is not None:
changed = self.hass.config_entries.async_update_entry(
entry, data={**entry.data, **updates}
)
if (
changed
and reload_on_update
and entry.state
in (
config_entries.ENTRY_STATE_LOADED,
config_entries.ENTRY_STATE_SETUP_RETRY,
)
):
self.hass.async_create_task(
self.hass.config_entries.async_reload(entry.entry_id)
)
return self.async_abort(reason="already_configured")

async def async_step_import(self, import_config=None):
"""Handle the initial step."""
host = import_config.get(CONF_HOST)
port = import_config[CONF_PORT]

status = self._abort_if_host_port_configured(port, host, import_config)
if status is not None:
return status

if host is not None:
name = f"{host}:{port}"
else:
name = port

return self.async_create_entry(title=name, data=import_config)
21 changes: 21 additions & 0 deletions homeassistant/components/dsmr/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Constants for the DSMR integration."""

DOMAIN = "dsmr"

PLATFORMS = ["sensor"]

CONF_DSMR_VERSION = "dsmr_version"
CONF_RECONNECT_INTERVAL = "reconnect_interval"
CONF_PRECISION = "precision"

DEFAULT_DSMR_VERSION = "2.2"
DEFAULT_PORT = "/dev/ttyUSB0"
DEFAULT_PRECISION = 3
DEFAULT_RECONNECT_INTERVAL = 30

DATA_TASK = "task"

ICON_GAS = "mdi:fire"
ICON_POWER = "mdi:flash"
ICON_POWER_FAILURE = "mdi:flash-off"
ICON_SWELL_SAG = "mdi:pulse"
3 changes: 2 additions & 1 deletion homeassistant/components/dsmr/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"name": "DSMR Slimme Meter",
"documentation": "https://www.home-assistant.io/integrations/dsmr",
"requirements": ["dsmr_parser==0.18"],
"codeowners": []
"codeowners": ["@Robbie1221"],
"config_flow": false
}
101 changes: 65 additions & 36 deletions homeassistant/components/dsmr/sensor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Support for Dutch Smart Meter (also known as Smartmeter or P1 port)."""
import asyncio
from asyncio import CancelledError
from functools import partial
import logging

Expand All @@ -9,6 +10,7 @@
import voluptuous as vol

from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
CONF_HOST,
CONF_PORT,
Expand All @@ -18,44 +20,57 @@
from homeassistant.core import CoreState, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import HomeAssistantType

from .const import (
CONF_DSMR_VERSION,
CONF_PRECISION,
CONF_RECONNECT_INTERVAL,
DATA_TASK,
DEFAULT_DSMR_VERSION,
DEFAULT_PORT,
DEFAULT_PRECISION,
DEFAULT_RECONNECT_INTERVAL,
DOMAIN,
ICON_GAS,
ICON_POWER,
ICON_POWER_FAILURE,
ICON_SWELL_SAG,
)

_LOGGER = logging.getLogger(__name__)

CONF_DSMR_VERSION = "dsmr_version"
CONF_RECONNECT_INTERVAL = "reconnect_interval"
CONF_PRECISION = "precision"

DEFAULT_DSMR_VERSION = "2.2"
DEFAULT_PORT = "/dev/ttyUSB0"
DEFAULT_PRECISION = 3

DOMAIN = "dsmr"

ICON_GAS = "mdi:fire"
ICON_POWER = "mdi:flash"
ICON_POWER_FAILURE = "mdi:flash-off"
ICON_SWELL_SAG = "mdi:pulse"

RECONNECT_INTERVAL = 5

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string,
vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_DSMR_VERSION, default=DEFAULT_DSMR_VERSION): vol.All(
cv.string, vol.In(["5B", "5", "4", "2.2"])
),
vol.Optional(CONF_RECONNECT_INTERVAL, default=30): int,
vol.Optional(CONF_RECONNECT_INTERVAL, default=DEFAULT_RECONNECT_INTERVAL): int,
vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int),
}
)


async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Import the platform into a config entry."""
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
)
)


async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
) -> None:
"""Set up the DSMR sensor."""
# Suppress logging
logging.getLogger("dsmr_parser").setLevel(logging.ERROR)

config = entry.data

dsmr_version = config[CONF_DSMR_VERSION]

# Define list of name,obis mappings to generate entities
Expand Down Expand Up @@ -141,40 +156,54 @@ async def connect_and_reconnect():
# Start DSMR asyncio.Protocol reader
try:
transport, protocol = await hass.loop.create_task(reader_factory())
except (
serial.serialutil.SerialException,
ConnectionRefusedError,
TimeoutError,
):
# Log any error while establishing connection and drop to retry
# connection wait
_LOGGER.exception("Error connecting to DSMR")
transport = None

if transport:
# Register listener to close transport on HA shutdown
stop_listener = hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, transport.close
)
if transport:
# Register listener to close transport on HA shutdown
stop_listener = hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, transport.close
)

# Wait for reader to close
await protocol.wait_closed()
# Wait for reader to close
await protocol.wait_closed()

if hass.state != CoreState.stopping:
# Unexpected disconnect
if transport:
# remove listener
stop_listener()

transport = None
protocol = None

# Reflect disconnect state in devices state by setting an
# empty telegram resulting in `unknown` states
update_entities_telegram({})

# throttle reconnect attempts
await asyncio.sleep(config[CONF_RECONNECT_INTERVAL])

except (serial.serialutil.SerialException, OSError):
# Log any error while establishing connection and drop to retry
# connection wait
_LOGGER.exception("Error connecting to DSMR")
transport = None
protocol = None
except CancelledError:
if stop_listener:
stop_listener()

if transport:
transport.close()

if protocol:
await protocol.wait_closed()

return

# Can't be hass.async_add_job because job runs forever
hass.loop.create_task(connect_and_reconnect())
task = hass.loop.create_task(connect_and_reconnect())

# Save the task to be able to cancel it when unloading
hass.data[DOMAIN][entry.entry_id][DATA_TASK] = task


class DSMREntity(Entity):
Expand Down
9 changes: 9 additions & 0 deletions homeassistant/components/dsmr/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"config": {
"step": {},
"error": {},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}
Loading

0 comments on commit d0120d5

Please sign in to comment.