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