Files
Home-Assistant/custom_components/wyzeapi/button.py
T
2026-06-12 09:59:05 -04:00

318 lines
11 KiB
Python

"""Platform for button integration."""
from collections.abc import Callable
import logging
from typing import Any
from aiohttp.client_exceptions import ClientConnectionError
from wyzeapy import Wyzeapy
from wyzeapy.services.irrigation_service import Irrigation, IrrigationService, Zone
from wyzeapy.services.switch_service import Switch
from homeassistant.components.button import ButtonDeviceClass, ButtonEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_registry import EntityCategory
from .const import CONF_CLIENT, DOMAIN, RESET_BUTTON_PRESSED
from .token_manager import token_exception_handler
_LOGGER = logging.getLogger(__name__)
ATTRIBUTION = "Data provided by Wyze"
OUTDOOR_PLUGS = ["WLPPO"]
@token_exception_handler
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: Callable[[list[Any], bool], None],
) -> None:
"""This function sets up the config entry.
:param hass: The Home Assistant Instance
:param config_entry: The current config entry
:param async_add_entities: This function adds entities to the config entry
:return:
"""
_LOGGER.debug("""Creating new Wyze button component""")
client: Wyzeapy = hass.data[DOMAIN][config_entry.entry_id][CONF_CLIENT]
irrigation_service = await client.irrigation_service
switch_service = await client.switch_service
# Get all irrigation devices
irrigation_devices = await irrigation_service.get_irrigations()
# Create a button entity for each zone in each irrigation device
buttons = []
for device in irrigation_devices:
# Update the device to get its zones
device = await irrigation_service.update(device)
# Add a button entity for each enabled zone in the irrigation device
buttons.extend(
[
WyzeIrrigationZoneButton(irrigation_service, device, zone)
for zone in device.zones
if zone.enabled
]
)
# Add a stop all schedules button for each irrigation device, not each zone
buttons.append(WyzeIrrigationStopAllButton(irrigation_service, device))
plugs = await switch_service.get_switches()
buttons.extend(
[
WyzePowerSensorResetButton(plug)
for plug in plugs
if plug.product_model in OUTDOOR_PLUGS
]
)
async_add_entities(buttons, True)
class WyzeIrrigationZoneButton(ButtonEntity):
"""Representation of a Wyze Irrigation Zone Button."""
_attr_has_entity_name = True
def __init__(
self, irrigation_service: IrrigationService, irrigation: Irrigation, zone: Zone
) -> None:
"""Initialize the irrigation zone button."""
self._irrigation_service = irrigation_service
self._device = irrigation
self._zone = zone
@property
def name(self) -> str:
"""Return the name of the zone."""
return f"{self._zone.name}"
@property
def unique_id(self) -> str:
"""Return a unique ID for the zone."""
return f"Start {self._device.mac}-zone-{self._zone.zone_number}"
@property
def device_info(self) -> DeviceInfo:
"""Return device information about this entity."""
return DeviceInfo(
identifiers={(DOMAIN, self._device.mac)},
name=self._device.nickname,
manufacturer="WyzeLabs",
model=self._device.product_model,
connections={(dr.CONNECTION_NETWORK_MAC, self._device.mac)},
)
@property
def device_class(self) -> str:
"""Return the device class of the button."""
return ButtonDeviceClass.RESTART
@property
def icon(self) -> str:
"""Return the icon for the zone start button."""
return "mdi:sprinkler"
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return entity specific state attributes."""
return {
"zone_number": self._zone.zone_number,
"zone_id": self._zone.zone_id,
"enabled": self._zone.enabled,
"quickrun_duration": self._zone.quickrun_duration,
}
async def async_press(self) -> None:
"""Start the zone with its quickrun duration.
This method is called when the button is pressed in Home Assistant.
It starts the irrigation zone for the duration specified in the corresponding number entity.
The number entity is found using the exact unique_id pattern that follows the format:
{device.mac}-zone-{zone.zone_number}-quickrun-duration
Raises:
HomeAssistantError: If the zone cannot be started or the number entity is invalid.
"""
try:
# The quickrun duration field doesnt exist in the Wyze API
# It has been created in Home Assistant as a number entity
# to conveniently trigger a zone start with a specific duration
# Get the device registry and find the device
device_registry = dr.async_get(self.hass)
device = device_registry.async_get_device(
identifiers={(DOMAIN, self._device.mac)}
)
if not device:
raise HomeAssistantError(f"Device not found for MAC {self._device.mac}")
# Get the entity registry
entity_registry = er.async_get(self.hass)
# Find the matching number entity using the zone number and device MAC
# The number entities have unique_id pattern: {device.mac}-zone-{zone.zone_number}-quickrun-duration
expected_unique_id = (
f"{self._device.mac}-zone-{self._zone.zone_number}-quickrun-duration"
)
matching_entity = None
for entity_id, entity in entity_registry.entities.items():
if (
entity.device_id == device.id
and entity.platform == DOMAIN
and entity_id.startswith("number.")
and entity.unique_id == expected_unique_id
):
matching_entity = entity_id
break
_LOGGER.debug(
"Looking for number entity with unique_id: %s", expected_unique_id
)
_LOGGER.debug("Found matching entity: %s", matching_entity)
if not matching_entity:
raise HomeAssistantError(
f"No number entity found for zone {self._zone.name} (zone {self._zone.zone_number}, device: {self._device.mac})"
)
# Get the current state of the number entity
state = self.hass.states.get(matching_entity)
if state is None or state.state in ["unavailable", "unknown"]:
raise HomeAssistantError(
f"Number entity {matching_entity} is unavailable or unknown"
)
# Convert duration from minutes to seconds
try:
duration_minutes = float(state.state)
if duration_minutes <= 0:
raise ValueError("Duration must be greater than 0")
duration_seconds = int(duration_minutes * 60)
except ValueError as err:
raise HomeAssistantError(
f"Invalid duration {state.state} for {matching_entity}"
) from err
_LOGGER.debug(
"Starting zone %s (zone %s) for %s minutes (%s seconds)",
self._zone.name,
self._zone.zone_number,
duration_minutes,
duration_seconds,
)
# Start the zone with the specified duration
await self._irrigation_service.start_zone(
self._device, self._zone.zone_number, duration_seconds
)
except HomeAssistantError as err:
_LOGGER.error("Failed to start zone %s: %s", self._zone.name, err)
raise
except Exception as err:
_LOGGER.error("Unexpected error starting zone %s: %s", self._zone.name, err)
raise HomeAssistantError(
f"Failed to start zone {self._zone.name}: {err}"
) from err
class WyzeIrrigationStopAllButton(ButtonEntity):
"""Representation of a Wyze Irrigation Stop All Schedules Button."""
_attr_name = "Stop All Zones"
def __init__(
self, irrigation_service: IrrigationService, irrigation: Irrigation
) -> None:
"""Initialize the irrigation stop all button."""
self._irrigation_service = irrigation_service
self._device = irrigation
@property
def unique_id(self) -> str:
"""Return a unique ID for the button."""
return f"Stop All {self._device.mac}"
@property
def device_info(self) -> DeviceInfo:
"""Return device information about this entity."""
return DeviceInfo(
identifiers={(DOMAIN, self._device.mac)},
name=self._device.nickname,
manufacturer="WyzeLabs",
model=self._device.product_model,
serial_number=self._device.sn,
connections={(dr.CONNECTION_NETWORK_MAC, self._device.mac)},
)
@property
def device_class(self) -> str:
"""Return the device class of the button."""
return ButtonDeviceClass.RESTART
@property
def icon(self) -> str:
"""Return the icon for the stop all button."""
return "mdi:octagon"
async def async_press(self) -> None:
"""Stop all running irrigation schedules.
This method is called when the button is pressed in Home Assistant.
It will stop all running irrigation schedules for the device.
Raises:
HomeAssistantError: If the schedules cannot be stopped.
"""
try:
await self._irrigation_service.stop_running_schedule(self._device)
except ClientConnectionError as err:
raise HomeAssistantError(f"Failed to stop schedules: {err}") from err
except Exception as err:
_LOGGER.error("Error stopping schedules: %s", err)
raise HomeAssistantError(f"Failed to stop schedules: {err}") from err
class WyzePowerSensorResetButton(ButtonEntity):
"""Wyze Power Sensor Reset Button."""
_attr_has_entity_name = True
_attr_should_poll = False
_attr_entity_category = EntityCategory.CONFIG
_attr_name = "Energy Usage Reset"
def __init__(self, switch: Switch) -> None:
"""Initialize a power sensor reset button."""
self._switch = switch
@property
def device_info(self) -> DeviceInfo:
"""Return device information about this entity."""
return DeviceInfo(
identifiers={(DOMAIN, self._switch.mac)},
name=self._switch.nickname,
)
@property
def unique_id(self) -> str:
"""Create a unique ID for the button."""
return f"{self._switch.mac} Reset button"
async def async_press(self) -> None:
"""Reset the sensor usage."""
async_dispatcher_send(
self.hass,
f"{RESET_BUTTON_PRESSED}-{self._switch.mac}",
self._switch,
)