Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update setup and ConfigEntry #1

Merged
merged 5 commits into from
Feb 11, 2024
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
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)
Copy link

Choose a reason for hiding this comment

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

Pourquoi ne pas faire de la fonction read_serial_number() une méthode de l'objet serial_reader ?
Vous pouvez d'ailleurs passer le timeout à 9, Home Assistant ne commence à afficher des warning d'initialisation qu'à partir de 10s.

# 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
Copy link

Choose a reason for hiding this comment

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

Ici le unique id concerne la config et non le device : cette configuration target bien sur le device (USB) et non sur le linky derrière. Je garderais SETUP_SERIAL comme identifiant de configuration je pense (d'ailleurs changer un uniq id dans HA entraine systématiquement un nouvel objet et une non continuité par rapport au précédents, sur des sensors cela veut dire un nouveau sensor).

Gardons l'actuel et enlevons ce TODO, cela évitera de mauvaises surprises à l'avenir.

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"
Copy link

Choose a reason for hiding this comment

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

La je veux en savoir plus :)

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