Source code for adi_lg_plugins.drivers.homeassistantdriver

"""
Driver to control power via a Home Assistant switch/outlet using the REST API.
"""

import time

import attr
import requests
from labgrid.driver.common import Driver
from labgrid.driver.powerdriver import PowerResetMixin
from labgrid.factory import target_factory
from labgrid.protocol import PowerProtocol
from labgrid.step import step


class HomeAssistantException(Exception):
    pass


[docs] class HomeAssistantClient: """Client for Home Assistant REST API switch control. :param url: Base URL of the Home Assistant instance :param token: Long-lived access token """
[docs] def __init__(self, url, token): self.url = url.rstrip("/") self.headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", } self._check_api()
def _check_api(self): """Verify Home Assistant API is reachable.""" try: resp = requests.get(f"{self.url}/api/", headers=self.headers, timeout=10) resp.raise_for_status() except requests.RequestException as e: raise HomeAssistantException( f"Failed to connect to Home Assistant at {self.url}: {e}" ) from e @staticmethod def _domain(entity_id): """Extract the domain from an entity_id (e.g. 'light.office' -> 'light').""" return entity_id.split(".", 1)[0]
[docs] def turn_on(self, entity_id): """Turn on an entity.""" domain = self._domain(entity_id) resp = requests.post( f"{self.url}/api/services/{domain}/turn_on", headers=self.headers, json={"entity_id": entity_id}, timeout=10, ) if not resp.ok: raise HomeAssistantException( f"Failed to turn on {entity_id}: {resp.status_code} {resp.text}" )
[docs] def turn_off(self, entity_id): """Turn off an entity.""" domain = self._domain(entity_id) resp = requests.post( f"{self.url}/api/services/{domain}/turn_off", headers=self.headers, json={"entity_id": entity_id}, timeout=10, ) if not resp.ok: raise HomeAssistantException( f"Failed to turn off {entity_id}: {resp.status_code} {resp.text}" )
[docs] def get_state(self, entity_id): """Get the current state of an entity. Returns: bool: True if the entity state is 'on', False otherwise. """ resp = requests.get( f"{self.url}/api/states/{entity_id}", headers=self.headers, timeout=10, ) if not resp.ok: raise HomeAssistantException( f"Failed to get state of {entity_id}: {resp.status_code} {resp.text}" ) return resp.json().get("state") == "on"
[docs] @target_factory.reg_driver @attr.s(eq=False) class HomeAssistantPowerDriver(Driver, PowerResetMixin, PowerProtocol): """HomeAssistantPowerDriver - Driver using a Home Assistant switch/outlet to control a target's power via the Home Assistant REST API.""" bindings = {"ha_outlet": {"HomeAssistantOutlet"}} def __attrs_post_init__(self): super().__attrs_post_init__() self.client = HomeAssistantClient(self.ha_outlet.url, self.ha_outlet.token)
[docs] @Driver.check_active @step() def on(self): """Turn on the configured Home Assistant switch.""" self.client.turn_on(self.ha_outlet.entity_id) self.logger.debug("Powered ON via Home Assistant entity %s", self.ha_outlet.entity_id)
[docs] @Driver.check_active @step() def off(self): """Turn off the configured Home Assistant switch.""" self.client.turn_off(self.ha_outlet.entity_id) self.logger.debug("Powered OFF via Home Assistant entity %s", self.ha_outlet.entity_id)
[docs] @Driver.check_active @step() def reset(self): """Power reset: off, delay, on.""" self.off() self.logger.debug("Waiting %.1f seconds before powering ON", self.ha_outlet.delay) time.sleep(self.ha_outlet.delay) self.on()
[docs] @Driver.check_active @step() def cycle(self): """Power cycle (same as reset).""" self.off() time.sleep(self.ha_outlet.delay) self.on()
[docs] @Driver.check_active @step() def get(self): """Get the current power state. Returns: bool: True if the switch is on, False otherwise. """ return self.client.get_state(self.ha_outlet.entity_id)