diff --git a/.coveragerc b/.coveragerc index 899c16e3920..e22611edf51 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1105,6 +1105,7 @@ omit = homeassistant/components/tractive/device_tracker.py homeassistant/components/tractive/entity.py homeassistant/components/tractive/sensor.py + homeassistant/components/tractive/switch.py homeassistant/components/tradfri/* homeassistant/components/trafikverket_train/sensor.py homeassistant/components/trafikverket_weatherstation/sensor.py diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index 5dcbd4574b3..be612ef5cc7 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -21,7 +21,10 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( + ATTR_BUZZER, ATTR_DAILY_GOAL, + ATTR_LED, + ATTR_LIVE_TRACKING, ATTR_MINUTES_ACTIVE, CLIENT, DOMAIN, @@ -33,7 +36,7 @@ from .const import ( TRACKER_POSITION_UPDATED, ) -PLATFORMS = ["binary_sensor", "device_tracker", "sensor"] +PLATFORMS = ["binary_sensor", "device_tracker", "sensor", "switch"] _LOGGER = logging.getLogger(__name__) @@ -43,10 +46,11 @@ _LOGGER = logging.getLogger(__name__) class Trackables: """A class that describes trackables.""" - trackable: dict | None = None - tracker_details: dict | None = None - hw_info: dict | None = None - pos_report: dict | None = None + tracker: aiotractive.tracker.Tracker + trackable: dict + tracker_details: dict + hw_info: dict + pos_report: dict async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -112,7 +116,7 @@ async def _generate_trackables(client, trackable): tracker.details(), tracker.hw_info(), tracker.pos_report() ) - return Trackables(trackable, tracker_details, hw_info, pos_report) + return Trackables(tracker, trackable, tracker_details, hw_info, pos_report) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -188,9 +192,13 @@ class TractiveClient: continue def _send_hardware_update(self, event): + # Sometimes hardware event doesn't contain complete data. payload = { ATTR_BATTERY_LEVEL: event["hardware"]["battery_level"], ATTR_BATTERY_CHARGING: event["charging_state"] == "CHARGING", + ATTR_LIVE_TRACKING: event.get("live_tracking", {}).get("active"), + ATTR_BUZZER: event.get("buzzer_control", {}).get("active"), + ATTR_LED: event.get("led_control", {}).get("active"), } self._dispatch_tracker_event( TRACKER_HARDWARE_STATUS_UPDATED, event["tracker_id"], payload diff --git a/homeassistant/components/tractive/const.py b/homeassistant/components/tractive/const.py index 7f1b5ddb4f2..6a61024cd51 100644 --- a/homeassistant/components/tractive/const.py +++ b/homeassistant/components/tractive/const.py @@ -7,6 +7,9 @@ DOMAIN = "tractive" RECONNECT_INTERVAL = timedelta(seconds=10) ATTR_DAILY_GOAL = "daily_goal" +ATTR_BUZZER = "buzzer" +ATTR_LED = "led" +ATTR_LIVE_TRACKING = "live_tracking" ATTR_MINUTES_ACTIVE = "minutes_active" CLIENT = "client" diff --git a/homeassistant/components/tractive/switch.py b/homeassistant/components/tractive/switch.py new file mode 100644 index 00000000000..d58e38a7cc9 --- /dev/null +++ b/homeassistant/components/tractive/switch.py @@ -0,0 +1,173 @@ +"""Support for Tractive switches.""" +from __future__ import annotations + +from dataclasses import dataclass +import logging +from typing import Any, Literal + +from aiotractive.exceptions import TractiveError + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import Trackables +from .const import ( + ATTR_BUZZER, + ATTR_LED, + ATTR_LIVE_TRACKING, + CLIENT, + DOMAIN, + SERVER_UNAVAILABLE, + TRACKABLES, + TRACKER_HARDWARE_STATUS_UPDATED, +) +from .entity import TractiveEntity + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class TractiveRequiredKeysMixin: + """Mixin for required keys.""" + + method: Literal["async_set_buzzer", "async_set_led", "async_set_live_tracking"] + + +@dataclass +class TractiveSwitchEntityDescription( + SwitchEntityDescription, TractiveRequiredKeysMixin +): + """Class describing Tractive switch entities.""" + + +SWITCH_TYPES: tuple[TractiveSwitchEntityDescription, ...] = ( + TractiveSwitchEntityDescription( + key=ATTR_BUZZER, + name="Tracker Buzzer", + icon="mdi:volume-high", + method="async_set_buzzer", + ), + TractiveSwitchEntityDescription( + key=ATTR_LED, + name="Tracker LED", + icon="mdi:led-on", + method="async_set_led", + ), + TractiveSwitchEntityDescription( + key=ATTR_LIVE_TRACKING, + name="Live Tracking", + icon="mdi:map-marker-path", + method="async_set_live_tracking", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Tractive switches.""" + client = hass.data[DOMAIN][entry.entry_id][CLIENT] + trackables = hass.data[DOMAIN][entry.entry_id][TRACKABLES] + + entities = [ + TractiveSwitch(client.user_id, item, description) + for description in SWITCH_TYPES + for item in trackables + ] + + async_add_entities(entities) + + +class TractiveSwitch(TractiveEntity, SwitchEntity): + """Tractive switch.""" + + entity_description: TractiveSwitchEntityDescription + + def __init__( + self, + user_id: str, + item: Trackables, + description: TractiveSwitchEntityDescription, + ) -> None: + """Initialize switch entity.""" + super().__init__(user_id, item.trackable, item.tracker_details) + + self._attr_name = f"{item.trackable['details']['name']} {description.name}" + self._attr_unique_id = f"{item.trackable['_id']}_{description.key}" + self._attr_available = False + self._tracker = item.tracker + self._method = getattr(self, description.method) + self.entity_description = description + + @callback + def handle_server_unavailable(self) -> None: + """Handle server unavailable.""" + self._attr_available = False + self.async_write_ha_state() + + @callback + def handle_hardware_status_update(self, event: dict[str, Any]) -> None: + """Handle hardware status update.""" + if (state := event[self.entity_description.key]) is None: + return + self._attr_is_on = state + self._attr_available = True + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{TRACKER_HARDWARE_STATUS_UPDATED}-{self._tracker_id}", + self.handle_hardware_status_update, + ) + ) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SERVER_UNAVAILABLE}-{self._user_id}", + self.handle_server_unavailable, + ) + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on a switch.""" + try: + result = await self._method(True) + except TractiveError as error: + _LOGGER.error(error) + return + # Write state back to avoid switch flips with a slow response + if result["pending"]: + self._attr_is_on = True + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off a switch.""" + try: + result = await self._method(False) + except TractiveError as error: + _LOGGER.error(error) + return + # Write state back to avoid switch flips with a slow response + if result["pending"]: + self._attr_is_on = False + self.async_write_ha_state() + + async def async_set_buzzer(self, active: bool) -> dict[str, Any]: + """Set the buzzer on/off.""" + return await self._tracker.set_buzzer_active(active) + + async def async_set_led(self, active: bool) -> dict[str, Any]: + """Set the LED on/off.""" + return await self._tracker.set_led_active(active) + + async def async_set_live_tracking(self, active: bool) -> dict[str, Any]: + """Set the live tracking on/off.""" + return await self._tracker.set_live_tracking_active(active)