Skip to content

Commit

Permalink
Merge pull request #1 from tomleglaunec/configentry
Browse files Browse the repository at this point in the history
Update setup and ConfigEntry
  • Loading branch information
tomleglaunec authored Feb 11, 2024
2 parents ee059b3 + 1aac339 commit da8544b
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 162 deletions.
82 changes: 72 additions & 10 deletions custom_components/linkytic/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
"""The linkytic integration."""

from __future__ import annotations
import asyncio

import logging

from homeassistant.config_entries import ConfigEntry
from homeassistant.config_entries import ConfigEntry, ConfigEntryNotReady
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import HomeAssistant
from homeassistant.components import usb

from .const import (
DOMAIN,
Expand All @@ -15,6 +18,7 @@
SETUP_TICMODE,
SETUP_PRODUCER,
TICMODE_STANDARD,
LINKY_IO_ERRORS,
)
from .serial_reader import LinkyTICReader

Expand All @@ -26,15 +30,40 @@
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up linkytic from a config entry."""
# Create the serial reader thread and start it
serial_reader = LinkyTICReader(
title=entry.title,
port=entry.data.get(SETUP_SERIAL),
std_mode=entry.data.get(SETUP_TICMODE) == TICMODE_STANDARD,
producer_mode=entry.data.get(SETUP_PRODUCER),
three_phase=entry.data.get(SETUP_THREEPHASE),
real_time=entry.options.get(OPTIONS_REALTIME),
)
serial_reader.start()
port = entry.data.get(SETUP_SERIAL)
try:
serial_reader = LinkyTICReader(
title=entry.title,
port=port,
std_mode=entry.data.get(SETUP_TICMODE) == TICMODE_STANDARD,
producer_mode=entry.data.get(SETUP_PRODUCER),
three_phase=entry.data.get(SETUP_THREEPHASE),
real_time=entry.options.get(OPTIONS_REALTIME),
)
serial_reader.start()

async def read_serial_number(serial: LinkyTICReader):
while serial.serial_number is None:
await asyncio.sleep(1)
return serial.serial_number

s_n = await asyncio.wait_for(read_serial_number(serial_reader), timeout=5)
# TODO: check if S/N is the one saved in config entry, if not this is a different meter!

# Error when opening serial port.
except LINKY_IO_ERRORS as e:
raise ConfigEntryNotReady(f"Couldn't open serial port {port}: {e}") from e

# Timeout waiting for S/N to be read.
except TimeoutError as e:
serial_reader.signalstop("linkytic_timeout")
del serial_reader
raise ConfigEntryNotReady(
f"Connected to serial port but coulnd't read serial number before timeout: check if TIC is connected and active."
)

_LOGGER.info(f"Device connected with serial number: {s_n}")

hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, serial_reader.signalstop)
# Add options callback
entry.async_on_unload(entry.add_update_listener(update_listener))
Expand Down Expand Up @@ -71,3 +100,36 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry):
return
# Update its options
serial_reader.update_options(entry.options.get(OPTIONS_REALTIME))


async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry):
"""Migrate old entry."""
_LOGGER.info(
"Migrating from version %d.%d", config_entry.version, config_entry.minor_version
)

if config_entry.version == 1:
new = {**config_entry.data}

if config_entry.minor_version < 2:
# Migrate to serial by-id.
serial_by_id = await hass.async_add_executor_job(
usb.get_serial_by_id, new[SETUP_SERIAL]
)
if serial_by_id == new[SETUP_SERIAL]:
_LOGGER.warning(
f"Couldn't find a persistent /dev/serial/by-id alias for {serial_by_id}. "
"Problems might occur at startup if device names are not persistent."
)
else:
new[SETUP_SERIAL] = serial_by_id

config_entry.minor_version = 2
hass.config_entries.async_update_entry(config_entry, data=new)

_LOGGER.info(
"Migration to version %d.%d successful",
config_entry.version,
config_entry.minor_version,
)
return True
2 changes: 1 addition & 1 deletion custom_components/linkytic/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def __init__(
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return DeviceInfo(
# connections={(DID_CONNECTION_TYPE, self._serial_controller._port)},
connections={(DID_CONNECTION_TYPE, self._serial_controller._port)},
identifiers={(DOMAIN, self._serial_controller.device_identification[DID_REGNUMBER])},
manufacturer=self._serial_controller.device_identification[DID_CONSTRUCTOR],
model=self._serial_controller.device_identification[DID_TYPE],
Expand Down
40 changes: 17 additions & 23 deletions custom_components/linkytic/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Config flow for linkytic integration."""

from __future__ import annotations

# import dataclasses
Expand All @@ -13,6 +14,7 @@
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import selector
from homeassistant.components import usb

from .const import (
DOMAIN,
Expand All @@ -39,12 +41,8 @@
vol.Required(SETUP_TICMODE, default=TICMODE_HISTORIC): selector.SelectSelector(
selector.SelectSelectorConfig(
options=[
selector.SelectOptionDict(
value=TICMODE_HISTORIC, label=TICMODE_HISTORIC_LABEL
),
selector.SelectOptionDict(
value=TICMODE_STANDARD, label=TICMODE_STANDARD_LABEL
),
selector.SelectOptionDict(value=TICMODE_HISTORIC, label=TICMODE_HISTORIC_LABEL),
selector.SelectOptionDict(value=TICMODE_STANDARD, label=TICMODE_STANDARD_LABEL),
]
),
),
Expand All @@ -58,43 +56,41 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for linkytic."""

VERSION = 1
MINOR_VERSION = 2

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
async def async_step_user(self, user_input: dict[str, Any] | None = None) -> FlowResult:
"""Handle the initial step."""
# No input
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
)
return self.async_show_form(step_id="user", data_schema=STEP_USER_DATA_SCHEMA)
# Validate input
await self.async_set_unique_id(DOMAIN + "_" + user_input[SETUP_SERIAL])
await self.async_set_unique_id(DOMAIN + "_" + user_input[SETUP_SERIAL]) # TODO: switch to meter s/n
self._abort_if_unique_id_configured()

# Search for serial/by-id, which SHOULD be a persistent name to serial interface.
_port = await self.hass.async_add_executor_job(usb.get_serial_by_id, user_input[SETUP_SERIAL])

errors = {}
title = user_input[SETUP_SERIAL]
try:
linky_tic_tester(
device=user_input[SETUP_SERIAL],
device=_port,
std_mode=user_input[SETUP_TICMODE] == TICMODE_STANDARD,
)
except CannotConnect as cannot_connect:
_LOGGER.error("%s: can not connect: %s", title, cannot_connect)
errors["base"] = "cannot_connect"
except CannotRead as cannot_read:
_LOGGER.error(
"%s: can not read a line after connection: %s", title, cannot_read
)
_LOGGER.error("%s: can not read a line after connection: %s", title, cannot_read)
errors["base"] = "cannot_read"
except Exception as exc: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception: %s", exc)
errors["base"] = "unknown"
else:
user_input[SETUP_SERIAL] = _port
return self.async_create_entry(title=title, data=user_input)

return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
return self.async_show_form(step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors)

# async def async_step_usb(self, discovery_info: UsbServiceInfo) -> FlowResult:
# """Handle a flow initialized by USB discovery."""
Expand All @@ -116,9 +112,7 @@ def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry

async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
async def async_step_init(self, user_input: dict[str, Any] | None = None) -> FlowResult:
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
Expand Down
13 changes: 8 additions & 5 deletions custom_components/linkytic/const.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
"""Constants for the linkytic integration."""

import serial
from termios import error
from serial import SerialException, SEVENBITS, PARITY_EVEN, STOPBITS_ONE

DOMAIN = "linkytic"

# Some termios exceptions are uncaught by pyserial
LINKY_IO_ERRORS = (SerialException, error)

# Config Flow

Expand All @@ -25,9 +28,9 @@
# Protocol configuration
# # https://www.enedis.fr/media/2035/download

BYTESIZE = serial.SEVENBITS
PARITY = serial.PARITY_EVEN
STOPBITS = serial.STOPBITS_ONE
BYTESIZE = SEVENBITS
PARITY = PARITY_EVEN
STOPBITS = STOPBITS_ONE

MODE_STANDARD_BAUD_RATE = 9600
MODE_STANDARD_FIELD_SEPARATOR = b"\x09"
Expand All @@ -36,7 +39,7 @@
MODE_HISTORIC_FIELD_SEPARATOR = b"\x20"

LINE_END = b"\r\n"
FRAME_END = b"\r\x03\x02\n"
FRAME_END = b"\x03\x02"

SHORT_FRAME_DETECTION_TAGS = ["ADIR1", "ADIR2", "ADIR3"]
SHORT_FRAME_FORCED_UPDATE_TAGS = [
Expand Down
8 changes: 4 additions & 4 deletions custom_components/linkytic/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1344,7 +1344,7 @@ def __init__(
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return DeviceInfo(
# connections={(DID_CONNECTION_TYPE, self._serial_controller._port)},
connections={(DID_CONNECTION_TYPE, self._serial_controller._port)},
identifiers={(DOMAIN, self._serial_controller.device_identification[DID_REGNUMBER] or "Unknown")},
manufacturer=self._serial_controller.device_identification[DID_CONSTRUCTOR],
model=self._serial_controller.device_identification[DID_TYPE],
Expand Down Expand Up @@ -1461,7 +1461,7 @@ def __init__(
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return DeviceInfo(
# connections={(DID_CONNECTION_TYPE, self._serial_controller._port)},
connections={(DID_CONNECTION_TYPE, self._serial_controller._port)},
identifiers={(DOMAIN, self._serial_controller.device_identification[DID_REGNUMBER] or "Unknown")},
manufacturer=self._serial_controller.device_identification[DID_CONSTRUCTOR],
model=self._serial_controller.device_identification[DID_TYPE],
Expand Down Expand Up @@ -1572,7 +1572,7 @@ def __init__(
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return DeviceInfo(
# connections={(DID_CONNECTION_TYPE, self._serial_controller._port)},
connections={(DID_CONNECTION_TYPE, self._serial_controller._port)},
identifiers={(DOMAIN, self._serial_controller.device_identification[DID_REGNUMBER] or "Unknown")},
manufacturer=self._serial_controller.device_identification[DID_CONSTRUCTOR],
model=self._serial_controller.device_identification[DID_TYPE],
Expand Down Expand Up @@ -1708,7 +1708,7 @@ def __init__(
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return DeviceInfo(
# connections={(DID_CONNECTION_TYPE, self._serial_controller._port)},
connections={(DID_CONNECTION_TYPE, self._serial_controller._port)},
identifiers={(DOMAIN, self._serial_controller.device_identification[DID_REGNUMBER] or "Unknown")},
manufacturer=self._serial_controller.device_identification[DID_CONSTRUCTOR],
model=self._serial_controller.device_identification[DID_TYPE],
Expand Down
Loading

0 comments on commit da8544b

Please sign in to comment.