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.
This commit is contained in:
Tomer 2024-12-27 21:48:36 +01:00 committed by GitHub
parent 6243c48ad1
commit aeb27b45d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 91 additions and 129 deletions

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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)