mirror of
https://github.com/thecode/ha-rpi_gpio.git
synced 2025-07-24 11:16:38 +00:00
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:
parent
6243c48ad1
commit
aeb27b45d6
@ -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)
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
Loading…
x
Reference in New Issue
Block a user