From aeb27b45d6f86b3228cfc96c79d0ce5c3fdddec4 Mon Sep 17 00:00:00 2001 From: Tomer <57483589+tomer-w@users.noreply.github.com> Date: Fri, 27 Dec 2024 21:48:36 +0100 Subject: [PATCH] Hub refactor (#332) * Hub refactor: As discussed in: https://github.com/thecode/ha-rpi_gpio/discussions/325 : 1. Each entity will do its own registration. 2. State can not be changed before we register for fd changes. 3. No need to call get_line_value from events. 4. Cover was changed to use the async APIs * Additional changes: 1. Add validationt to switch __init__ so if the line is busy it will fail early and not in async_added_to_hass 2. Remove the need to send to hub the HA entities. This makes the hub more generic. 3. Fix cover initial state to be read from the binary sensor instead of assuming the cover is closed. --- custom_components/rpi_gpio/binary_sensor.py | 17 ++- custom_components/rpi_gpio/cover.py | 56 +++++---- custom_components/rpi_gpio/hub.py | 125 +++++--------------- custom_components/rpi_gpio/switch.py | 22 ++-- 4 files changed, 91 insertions(+), 129 deletions(-) diff --git a/custom_components/rpi_gpio/binary_sensor.py b/custom_components/rpi_gpio/binary_sensor.py index a63d445..a6ec723 100644 --- a/custom_components/rpi_gpio/binary_sensor.py +++ b/custom_components/rpi_gpio/binary_sensor.py @@ -12,6 +12,7 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import CONF_SENSORS, CONF_NAME, CONF_PORT, CONF_UNIQUE_ID from .hub import BIAS + CONF_INVERT_LOGIC = "invert_logic" DEFAULT_INVERT_LOGIC = False CONF_BOUNCETIME = "bouncetime" @@ -78,12 +79,22 @@ class GPIODBinarySensor(BinarySensorEntity): self._active_low = active_low self._bias = bias self._debounce = debounce + self._line, current_is_on = self._hub.add_sensor(self._port, self._active_low, self._bias, self._debounce) + self._attr_is_on = current_is_on async def async_added_to_hass(self) -> None: await super().async_added_to_hass() - self._hub.add_sensor(self, self._port, self._active_low, self._bias, self._debounce) - self.async_write_ha_state() + _LOGGER.debug(f"GPIODBinarySensor async_added_to_hass: Adding fd:{self._line.fd}") + self._hub._hass.loop.add_reader(self._line.fd, self.handle_event) + + async def async_will_remove_from_hass(self) -> None: + await super().async_will_remove_from_hass() + _LOGGER.debug(f"GPIODBinarySensor async_will_remove_from_hass: Removing fd:{self._line.fd}") + self._hub._hass.loop.remove_reader(self._line.fd) + self._line.release() def handle_event(self): - self._attr_is_on = self._hub.get_line_value(self._port) + for event in self._line.read_edge_events(): + self._attr_is_on = True if event.event_type is event.Type.RISING_EDGE else False + _LOGGER.debug(f"Event: {event}. New line value: {self._attr_is_on}") self.schedule_update_ha_state(False) diff --git a/custom_components/rpi_gpio/cover.py b/custom_components/rpi_gpio/cover.py index e040693..e8e8d47 100644 --- a/custom_components/rpi_gpio/cover.py +++ b/custom_components/rpi_gpio/cover.py @@ -17,6 +17,7 @@ from homeassistant.const import CONF_COVERS, CONF_NAME, CONF_UNIQUE_ID from .hub import BIAS, DRIVE import homeassistant.helpers.config_validation as cv import voluptuous as vol +import asyncio CONF_RELAY_PIN = "relay_pin" CONF_RELAY_TIME = "relay_time" @@ -105,51 +106,66 @@ class GPIODCover(CoverEntity): self._state_port = state_port self._state_bias = state_bias self._state_active_low = state_active_low - self._attr_is_closed = False != state_active_low + self._relay_line, self._state_line, current_is_on = self._hub.add_cover( + self._relay_port, self._relay_active_low, self._relay_bias, self._relay_drive, + self._state_port, self._state_bias, self._state_active_low) + self._attr_is_closed = current_is_on + self.is_on = current_is_on async def async_added_to_hass(self) -> None: await super().async_added_to_hass() - self._hub.add_cover(self, self._relay_port, self._relay_active_low, self._relay_bias, - self._relay_drive, self._state_port, self._state_bias, self._state_active_low) - self.async_write_ha_state() + _LOGGER.debug(f"GPIODCover async_added_to_hass: Adding fd:{self._state_line.fd}") + self._hub._hass.loop.add_reader(self._state_line.fd, self.handle_event) + + async def async_will_remove_from_hass(self) -> None: + await super().async_will_remove_from_hass() + _LOGGER.debug(f"GPIODCover async_will_remove_from_hass: Removing fd:{self._state_line.fd}") + self._hub._hass.loop.remove_reader(self._state_line.fd) + self._relay_line.release() + self._state_line.release() def handle_event(self): - self._attr_is_closed = self._hub.get_line_value(self._state_port) + for event in self._state_line.read_edge_events(): + self._attr_is_closed = True if event.event_type is event.Type.RISING_EDGE else False + _LOGGER.debug(f"Event: {event}. New _attr_is_closed value: {self._attr_is_closed}") self.schedule_update_ha_state(False) - def close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs): + _LOGGER.debug(f"GPIODCover async_close_cover: is_closed: {self.is_closed}. is_closing: {self.is_closing}, is_opening: {self.is_opening}") if self.is_closed: return - self._hub.turn_on(self._relay_port) + self._hub.turn_on(self._relay_line, self._relay_port) self._attr_is_closing = True - self.schedule_update_ha_state(False) - sleep(self._relay_time) + self.async_write_ha_state() + await asyncio.sleep(self._relay_time) if not self.is_closing: # closing stopped return - self._hub.turn_off(self._relay_port) + self._hub.turn_off(self._relay_line, self._relay_port) self._attr_is_closing = False - self.handle_event() + self.async_write_ha_state() - def open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs): + _LOGGER.debug(f"GPIODCover async_open_cover: is_closed: {self.is_closed}. is_closing: {self.is_closing}, is_opening: {self.is_opening}") if not self.is_closed: return - self._hub.turn_on(self._relay_port) + self._hub.turn_on(self._relay_line, self._relay_port) self._attr_is_opening = True - self.schedule_update_ha_state(False) - sleep(self._relay_time) + self.async_write_ha_state() + await asyncio.sleep(self._relay_time) if not self.is_opening: # opening stopped return - self._hub.turn_off(self._relay_port) + self._hub.turn_off(self._relay_line, self._relay_port) self._attr_is_opening = False - self.handle_event() + self.async_write_ha_state() - def stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs): + _LOGGER.debug(f"GPIODCover async_stop_cover: is_closed: {self.is_closed}. is_closing: {self.is_closing}, is_opening: {self.is_opening}") if not (self.is_closing or self.is_opening): return - self._hub.turn_off(self._relay_port) + self._hub.turn_off(self._relay_line, self._relay_port) self._attr_is_opening = False self._attr_is_closing = False - self.schedule_update_ha_state(False) + self.async_write_ha_state() diff --git a/custom_components/rpi_gpio/hub.py b/custom_components/rpi_gpio/hub.py index d0823c2..d0fc0fd 100644 --- a/custom_components/rpi_gpio/hub.py +++ b/custom_components/rpi_gpio/hub.py @@ -39,10 +39,6 @@ class Hub: self._id = path self._hass = hass self._online = False - self._lines : gpiod.LineRequest = None - self._config : Dict[int, gpiod.LineSettings] = {} - self._edge_events = False - self._entities = {} if path: # use config @@ -62,13 +58,8 @@ class Hub: break self.verify_online() - _LOGGER.debug(f"using gpio_device: {self._path}") - # startup and shutdown triggers of hass - self._hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, self.startup) - self._hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.cleanup) - def verify_online(self): if not self._online: _LOGGER.error("No gpio device detected, bailing out") @@ -96,120 +87,60 @@ class Hub: _LOGGER.error(f"Port {port} already in use by {info.consumer}") raise HomeAssistantError(f"Port {port} already in use by {info.consumer}") - - async def startup(self, _): - """Stuff to do after starting.""" - _LOGGER.debug(f"startup {DOMAIN} hub") - if not self._online: - _LOGGER.debug(f"integration is not online") - return - if not self._config: - _LOGGER.debug(f"gpiod config is empty") - return - - # setup lines - try: - self.update_lines() - except Exception as e: - _LOGGER.error(f"Failed to update lines: {e}") - return - - if not self._edge_events: - return - - _LOGGER.debug("Start listener") - self._hass.loop.add_reader(self._lines.fd, self.handle_events) - - def cleanup(self, _): - """Stuff to do before stopping.""" - _LOGGER.debug(f"cleanup {DOMAIN} hub") - if self._config: - self._config.clear() - if self._lines: - self._lines.release() - if self._chip: - self._chip.close() - self._online = False - @property def hub_id(self) -> str: """ID for hub""" return self._id - def update_lines(self) -> None: - if self._lines: - self._lines.release() - - _LOGGER.debug(f"updating lines: {self._config}") - self._lines = self._chip.request_lines( - consumer = DOMAIN, - config = self._config - ) - _LOGGER.debug(f"update_lines new lines: {self._lines}") - - def handle_events(self): - for event in self._lines.read_edge_events(): - _LOGGER.debug(f"Event: {event}") - self._entities[event.line_offset].handle_event() - - def add_switch(self, entity, port, active_low, bias, drive_mode, init_output_value = True) -> None: - _LOGGER.debug(f"in add_switch {port}") + def add_switch(self, port, active_low, bias, drive_mode, init_state) -> gpiod.LineRequest: + _LOGGER.debug(f"add_switch - port: {port}, active_low: {active_low}, bias: {bias}, drive_mode: {drive_mode}, init_state: {init_state}") self.verify_online() self.verify_port_ready(port) - self._entities[port] = entity - self._config[port] = gpiod.LineSettings( + line_request = self._chip.request_lines( + consumer=DOMAIN, + config={port: gpiod.LineSettings( direction = Direction.OUTPUT, bias = BIAS[bias], drive = DRIVE[drive_mode], active_low = active_low, - output_value = Value.ACTIVE if init_output_value and entity.is_on else Value.INACTIVE - ) + output_value = Value.ACTIVE if init_state is not None and init_state else Value.INACTIVE)}) + _LOGGER.debug(f"add_switch line_request: {line_request}") + return line_request - def turn_on(self, port) -> None: + def turn_on(self, line, port) -> None: _LOGGER.debug(f"in turn_on {port}") self.verify_online() - self._lines.set_value(port, Value.ACTIVE) + line.set_value(port, Value.ACTIVE) - def turn_off(self, port) -> None: + def turn_off(self, line, port) -> None: _LOGGER.debug(f"in turn_off {port}") self.verify_online() - self._lines.set_value(port, Value.INACTIVE) + line.set_value(port, Value.INACTIVE) - def add_sensor(self, entity, port, active_low, bias, debounce) -> None: - _LOGGER.debug(f"in add_sensor {port}") + def add_sensor(self, port, active_low, bias, debounce) -> gpiod.LineRequest: + _LOGGER.debug(f"add_sensor - port: {port}, active_low: {active_low}, bias: {bias}, debounce: {debounce}") self.verify_online() self.verify_port_ready(port) - self._entities[port] = entity - self._config[port] = gpiod.LineSettings( - direction = Direction.INPUT, - edge_detection = Edge.BOTH, - bias = BIAS[bias], - active_low = active_low, - debounce_period = timedelta(milliseconds=debounce), - event_clock = Clock.REALTIME - ) - - # read current status of the sensor - with self._chip.request_lines( + line_request = self._chip.request_lines( consumer=DOMAIN, config={port: gpiod.LineSettings( direction = Direction.INPUT, + edge_detection = Edge.BOTH, bias = BIAS[bias], - active_low = active_low)}, - ) as request: - entity.is_on = True if request.get_value(port) == Value.ACTIVE else False + active_low = active_low, + debounce_period = timedelta(milliseconds=debounce), + event_clock = Clock.REALTIME)}) - _LOGGER.debug(f"current value for port {port}: {entity.is_on}") - self._edge_events = True + current_is_on = True if line_request.get_value(port) == Value.ACTIVE else False + _LOGGER.debug(f"add_sensor line_request: {line_request}. current state: {current_is_on}") + return line_request, current_is_on - def get_line_value(self, port): - return self._lines.get_value(port) == Value.ACTIVE - - def add_cover(self, entity, relay_port, relay_active_low, relay_bias, relay_drive, - state_port, state_bias, state_active_low) -> None: - _LOGGER.debug(f"in add_cover {relay_port} {state_port}") - self.add_switch(entity, relay_port, relay_active_low, relay_bias, relay_drive, init_output_value = False) - self.add_sensor(entity, state_port, state_active_low, state_bias, 50) + def add_cover(self, relay_port, relay_active_low, relay_bias, relay_drive, + state_port, state_bias, state_active_low): + _LOGGER.debug(f"add_cover - relay_port: {relay_port}, state_port: {state_port}") + relay_line = self.add_switch(relay_port, relay_active_low, relay_bias, relay_drive, False) + state_line, current_is_on = self.add_sensor(state_port, state_active_low, state_bias, 50) + return relay_line, state_line, current_is_on diff --git a/custom_components/rpi_gpio/switch.py b/custom_components/rpi_gpio/switch.py index c7af800..57d86c6 100644 --- a/custom_components/rpi_gpio/switch.py +++ b/custom_components/rpi_gpio/switch.py @@ -85,7 +85,9 @@ class GPIODSwitch(SwitchEntity, RestoreEntity): self._bias = bias self._drive_mode = drive self._persistent = persistent - + self._line = None + self._hub.verify_port_ready(self._port) + async def async_added_to_hass(self) -> None: """Call when the switch is added to hass.""" await super().async_added_to_hass() @@ -95,19 +97,21 @@ class GPIODSwitch(SwitchEntity, RestoreEntity): else: _LOGGER.debug(f"setting initial persistent state for: {self._port}. state: {state.state}") self._attr_is_on = True if state.state == STATE_ON else False - self._hub.add_switch(self, self._port, self._active_low, self._bias, self._drive_mode) - self.async_write_ha_state() + self.async_write_ha_state() + self._line = self._hub.add_switch(self._port, self._active_low, self._bias, self._drive_mode, self._attr_is_on) + + async def async_will_remove_from_hass(self) -> None: + await super().async_will_remove_from_hass() + _LOGGER.debug(f"GPIODSwitch async_will_remove_from_hass") + if self._line: + self._line.release() async def async_turn_on(self, **kwargs: Any) -> None: - self._hub.turn_on(self._port) + self._hub.turn_on(self._line, self._port) self._attr_is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: - self._hub.turn_off(self._port) + self._hub.turn_off(self._line, self._port) self._attr_is_on = False self.async_write_ha_state() - - def handle_event(self): - self._attr_is_on = self._hub.get_line_value(self._port) - self.schedule_update_ha_state(False) \ No newline at end of file