Add Rachio smart hose timer support (#107901)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
3c13a28357
commit
dbb4cf0ee7
9 changed files with 263 additions and 17 deletions
|
@ -1062,6 +1062,7 @@ omit =
|
||||||
homeassistant/components/rabbitair/fan.py
|
homeassistant/components/rabbitair/fan.py
|
||||||
homeassistant/components/rachio/__init__.py
|
homeassistant/components/rachio/__init__.py
|
||||||
homeassistant/components/rachio/binary_sensor.py
|
homeassistant/components/rachio/binary_sensor.py
|
||||||
|
homeassistant/components/rachio/coordinator.py
|
||||||
homeassistant/components/rachio/device.py
|
homeassistant/components/rachio/device.py
|
||||||
homeassistant/components/rachio/entity.py
|
homeassistant/components/rachio/entity.py
|
||||||
homeassistant/components/rachio/switch.py
|
homeassistant/components/rachio/switch.py
|
||||||
|
|
|
@ -83,7 +83,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
raise ConfigEntryNotReady from error
|
raise ConfigEntryNotReady from error
|
||||||
|
|
||||||
# Check for Rachio controller devices
|
# Check for Rachio controller devices
|
||||||
if not person.controllers:
|
if not person.controllers and not person.base_stations:
|
||||||
_LOGGER.error("No Rachio devices found in account %s", person.username)
|
_LOGGER.error("No Rachio devices found in account %s", person.username)
|
||||||
return False
|
return False
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
|
@ -91,10 +91,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"%d Rachio device(s) found; The url %s must be accessible from the internet"
|
"%d Rachio device(s) found; The url %s must be accessible from the internet"
|
||||||
" in order to receive updates"
|
" in order to receive updates"
|
||||||
),
|
),
|
||||||
len(person.controllers),
|
len(person.controllers) + len(person.base_stations),
|
||||||
webhook_url,
|
webhook_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for base in person.base_stations:
|
||||||
|
await base.coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
# Enable platform
|
# Enable platform
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = person
|
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = person
|
||||||
async_register_webhook(hass, entry)
|
async_register_webhook(hass, entry)
|
||||||
|
|
|
@ -26,6 +26,7 @@ KEY_NAME = "name"
|
||||||
KEY_MODEL = "model"
|
KEY_MODEL = "model"
|
||||||
KEY_ON = "on"
|
KEY_ON = "on"
|
||||||
KEY_DURATION = "totalDuration"
|
KEY_DURATION = "totalDuration"
|
||||||
|
KEY_DURATION_MINUTES = "duration"
|
||||||
KEY_RAIN_DELAY = "rainDelayExpirationDate"
|
KEY_RAIN_DELAY = "rainDelayExpirationDate"
|
||||||
KEY_RAIN_DELAY_END = "endTime"
|
KEY_RAIN_DELAY_END = "endTime"
|
||||||
KEY_RAIN_SENSOR_TRIPPED = "rainSensorTripped"
|
KEY_RAIN_SENSOR_TRIPPED = "rainSensorTripped"
|
||||||
|
@ -47,6 +48,21 @@ KEY_CUSTOM_SHADE = "customShade"
|
||||||
KEY_CUSTOM_CROP = "customCrop"
|
KEY_CUSTOM_CROP = "customCrop"
|
||||||
KEY_CUSTOM_SLOPE = "customSlope"
|
KEY_CUSTOM_SLOPE = "customSlope"
|
||||||
|
|
||||||
|
# Smart Hose timer
|
||||||
|
KEY_BASE_STATIONS = "baseStations"
|
||||||
|
KEY_VALVES = "valves"
|
||||||
|
KEY_REPORTED_STATE = "reportedState"
|
||||||
|
KEY_STATE = "state"
|
||||||
|
KEY_CONNECTED = "connected"
|
||||||
|
KEY_CURRENT_STATUS = "lastWateringAction"
|
||||||
|
KEY_DETECT_FLOW = "detectFlow"
|
||||||
|
KEY_BATTERY_STATUS = "batteryStatus"
|
||||||
|
KEY_REASON = "reason"
|
||||||
|
KEY_DEFAULT_RUNTIME = "defaultRuntimeSeconds"
|
||||||
|
KEY_DURATION_SECONDS = "durationSeconds"
|
||||||
|
KEY_FLOW_DETECTED = "flowDetected"
|
||||||
|
KEY_START_TIME = "start"
|
||||||
|
|
||||||
STATUS_ONLINE = "ONLINE"
|
STATUS_ONLINE = "ONLINE"
|
||||||
|
|
||||||
MODEL_GENERATION_1 = "GENERATION1"
|
MODEL_GENERATION_1 = "GENERATION1"
|
||||||
|
@ -56,6 +72,7 @@ SERVICE_PAUSE_WATERING = "pause_watering"
|
||||||
SERVICE_RESUME_WATERING = "resume_watering"
|
SERVICE_RESUME_WATERING = "resume_watering"
|
||||||
SERVICE_STOP_WATERING = "stop_watering"
|
SERVICE_STOP_WATERING = "stop_watering"
|
||||||
SERVICE_SET_ZONE_MOISTURE = "set_zone_moisture_percent"
|
SERVICE_SET_ZONE_MOISTURE = "set_zone_moisture_percent"
|
||||||
|
SERVICE_START_WATERING = "start_watering"
|
||||||
SERVICE_START_MULTIPLE_ZONES = "start_multiple_zone_schedule"
|
SERVICE_START_MULTIPLE_ZONES = "start_multiple_zone_schedule"
|
||||||
|
|
||||||
SIGNAL_RACHIO_UPDATE = f"{DOMAIN}_update"
|
SIGNAL_RACHIO_UPDATE = f"{DOMAIN}_update"
|
||||||
|
|
56
homeassistant/components/rachio/coordinator.py
Normal file
56
homeassistant/components/rachio/coordinator.py
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
"""Coordinator object for the Rachio integration."""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from rachiopy import Rachio
|
||||||
|
from requests.exceptions import Timeout
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.debounce import Debouncer
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
from .const import DOMAIN, KEY_ID, KEY_VALVES
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
UPDATE_DELAY_TIME = 8
|
||||||
|
|
||||||
|
|
||||||
|
class RachioUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||||
|
"""Coordinator Class for Rachio Hose Timers."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
rachio: Rachio,
|
||||||
|
base_station,
|
||||||
|
base_count: int,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the Rachio Update Coordinator."""
|
||||||
|
self.hass = hass
|
||||||
|
self.rachio = rachio
|
||||||
|
self.base_station = base_station
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
name=f"{DOMAIN} update coordinator",
|
||||||
|
# To avoid exceeding the rate limit, increase polling interval for
|
||||||
|
# each additional base station on the account
|
||||||
|
update_interval=timedelta(minutes=(base_count + 1)),
|
||||||
|
# Debouncer used because the API takes a bit to update state changes
|
||||||
|
request_refresh_debouncer=Debouncer(
|
||||||
|
hass, _LOGGER, cooldown=UPDATE_DELAY_TIME, immediate=False
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> dict[str, Any]:
|
||||||
|
"""Update smart hose timer data."""
|
||||||
|
try:
|
||||||
|
data = await self.hass.async_add_executor_job(
|
||||||
|
self.rachio.valve.list_valves, self.base_station[KEY_ID]
|
||||||
|
)
|
||||||
|
except Timeout as err:
|
||||||
|
raise UpdateFailed(f"Could not connect to the Rachio API: {err}") from err
|
||||||
|
return {valve[KEY_ID]: valve for valve in data[1][KEY_VALVES]}
|
|
@ -17,6 +17,7 @@ from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
KEY_BASE_STATIONS,
|
||||||
KEY_DEVICES,
|
KEY_DEVICES,
|
||||||
KEY_ENABLED,
|
KEY_ENABLED,
|
||||||
KEY_EXTERNAL_ID,
|
KEY_EXTERNAL_ID,
|
||||||
|
@ -37,6 +38,7 @@ from .const import (
|
||||||
SERVICE_STOP_WATERING,
|
SERVICE_STOP_WATERING,
|
||||||
WEBHOOK_CONST_ID,
|
WEBHOOK_CONST_ID,
|
||||||
)
|
)
|
||||||
|
from .coordinator import RachioUpdateCoordinator
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -67,6 +69,7 @@ class RachioPerson:
|
||||||
self.username = None
|
self.username = None
|
||||||
self._id: str | None = None
|
self._id: str | None = None
|
||||||
self._controllers: list[RachioIro] = []
|
self._controllers: list[RachioIro] = []
|
||||||
|
self._base_stations: list[RachioBaseStation] = []
|
||||||
|
|
||||||
async def async_setup(self, hass: HomeAssistant) -> None:
|
async def async_setup(self, hass: HomeAssistant) -> None:
|
||||||
"""Create rachio devices and services."""
|
"""Create rachio devices and services."""
|
||||||
|
@ -78,30 +81,34 @@ class RachioPerson:
|
||||||
can_pause = True
|
can_pause = True
|
||||||
break
|
break
|
||||||
|
|
||||||
all_devices = [rachio_iro.name for rachio_iro in self._controllers]
|
all_controllers = [rachio_iro.name for rachio_iro in self._controllers]
|
||||||
|
|
||||||
def pause_water(service: ServiceCall) -> None:
|
def pause_water(service: ServiceCall) -> None:
|
||||||
"""Service to pause watering on all or specific controllers."""
|
"""Service to pause watering on all or specific controllers."""
|
||||||
duration = service.data[ATTR_DURATION]
|
duration = service.data[ATTR_DURATION]
|
||||||
devices = service.data.get(ATTR_DEVICES, all_devices)
|
devices = service.data.get(ATTR_DEVICES, all_controllers)
|
||||||
for iro in self._controllers:
|
for iro in self._controllers:
|
||||||
if iro.name in devices:
|
if iro.name in devices:
|
||||||
iro.pause_watering(duration)
|
iro.pause_watering(duration)
|
||||||
|
|
||||||
def resume_water(service: ServiceCall) -> None:
|
def resume_water(service: ServiceCall) -> None:
|
||||||
"""Service to resume watering on all or specific controllers."""
|
"""Service to resume watering on all or specific controllers."""
|
||||||
devices = service.data.get(ATTR_DEVICES, all_devices)
|
devices = service.data.get(ATTR_DEVICES, all_controllers)
|
||||||
for iro in self._controllers:
|
for iro in self._controllers:
|
||||||
if iro.name in devices:
|
if iro.name in devices:
|
||||||
iro.resume_watering()
|
iro.resume_watering()
|
||||||
|
|
||||||
def stop_water(service: ServiceCall) -> None:
|
def stop_water(service: ServiceCall) -> None:
|
||||||
"""Service to stop watering on all or specific controllers."""
|
"""Service to stop watering on all or specific controllers."""
|
||||||
devices = service.data.get(ATTR_DEVICES, all_devices)
|
devices = service.data.get(ATTR_DEVICES, all_controllers)
|
||||||
for iro in self._controllers:
|
for iro in self._controllers:
|
||||||
if iro.name in devices:
|
if iro.name in devices:
|
||||||
iro.stop_watering()
|
iro.stop_watering()
|
||||||
|
|
||||||
|
# If only hose timers on account, none of these services apply
|
||||||
|
if not all_controllers:
|
||||||
|
return
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_STOP_WATERING,
|
SERVICE_STOP_WATERING,
|
||||||
|
@ -145,6 +152,9 @@ class RachioPerson:
|
||||||
raise ConfigEntryNotReady(f"API Error: {data}")
|
raise ConfigEntryNotReady(f"API Error: {data}")
|
||||||
self.username = data[1][KEY_USERNAME]
|
self.username = data[1][KEY_USERNAME]
|
||||||
devices: list[dict[str, Any]] = data[1][KEY_DEVICES]
|
devices: list[dict[str, Any]] = data[1][KEY_DEVICES]
|
||||||
|
base_station_data = rachio.valve.list_base_stations(self._id)
|
||||||
|
base_stations: list[dict[str, Any]] = base_station_data[1][KEY_BASE_STATIONS]
|
||||||
|
|
||||||
for controller in devices:
|
for controller in devices:
|
||||||
webhooks = rachio.notification.get_device_webhook(controller[KEY_ID])[1]
|
webhooks = rachio.notification.get_device_webhook(controller[KEY_ID])[1]
|
||||||
# The API does not provide a way to tell if a controller is shared
|
# The API does not provide a way to tell if a controller is shared
|
||||||
|
@ -174,6 +184,14 @@ class RachioPerson:
|
||||||
rachio_iro.setup()
|
rachio_iro.setup()
|
||||||
self._controllers.append(rachio_iro)
|
self._controllers.append(rachio_iro)
|
||||||
|
|
||||||
|
base_count = len(base_stations)
|
||||||
|
self._base_stations.extend(
|
||||||
|
RachioBaseStation(
|
||||||
|
rachio, base, RachioUpdateCoordinator(hass, rachio, base, base_count)
|
||||||
|
)
|
||||||
|
for base in base_stations
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER.info('Using Rachio API as user "%s"', self.username)
|
_LOGGER.info('Using Rachio API as user "%s"', self.username)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -186,6 +204,11 @@ class RachioPerson:
|
||||||
"""Get a list of controllers managed by this account."""
|
"""Get a list of controllers managed by this account."""
|
||||||
return self._controllers
|
return self._controllers
|
||||||
|
|
||||||
|
@property
|
||||||
|
def base_stations(self) -> list[RachioBaseStation]:
|
||||||
|
"""List of smart hose timer base stations."""
|
||||||
|
return self._base_stations
|
||||||
|
|
||||||
def start_multiple_zones(self, zones) -> None:
|
def start_multiple_zones(self, zones) -> None:
|
||||||
"""Start multiple zones."""
|
"""Start multiple zones."""
|
||||||
self.rachio.zone.start_multiple(zones)
|
self.rachio.zone.start_multiple(zones)
|
||||||
|
@ -321,6 +344,28 @@ class RachioIro:
|
||||||
_LOGGER.debug("Resuming watering on %s", self)
|
_LOGGER.debug("Resuming watering on %s", self)
|
||||||
|
|
||||||
|
|
||||||
|
class RachioBaseStation:
|
||||||
|
"""Represent a smart hose timer base station."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, rachio: Rachio, data: dict[str, Any], coordinator: RachioUpdateCoordinator
|
||||||
|
) -> None:
|
||||||
|
"""Initialize a hose time base station."""
|
||||||
|
self.rachio = rachio
|
||||||
|
self._id = data[KEY_ID]
|
||||||
|
self.serial_number = data[KEY_SERIAL_NUMBER]
|
||||||
|
self.mac_address = data[KEY_MAC_ADDRESS]
|
||||||
|
self.coordinator = coordinator
|
||||||
|
|
||||||
|
def start_watering(self, valve_id: str, duration: int) -> None:
|
||||||
|
"""Start watering on this valve."""
|
||||||
|
self.rachio.valve.start_watering(valve_id, duration)
|
||||||
|
|
||||||
|
def stop_watering(self, valve_id: str) -> None:
|
||||||
|
"""Stop watering on this valve."""
|
||||||
|
self.rachio.valve.stop_watering(valve_id)
|
||||||
|
|
||||||
|
|
||||||
def is_invalid_auth_code(http_status_code: int) -> bool:
|
def is_invalid_auth_code(http_status_code: int) -> bool:
|
||||||
"""HTTP status codes that mean invalid auth."""
|
"""HTTP status codes that mean invalid auth."""
|
||||||
return http_status_code in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN)
|
return http_status_code in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN)
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
"start_multiple_zone_schedule": "mdi:play",
|
"start_multiple_zone_schedule": "mdi:play",
|
||||||
"pause_watering": "mdi:pause",
|
"pause_watering": "mdi:pause",
|
||||||
"resume_watering": "mdi:play",
|
"resume_watering": "mdi:play",
|
||||||
"stop_watering": "mdi:stop"
|
"stop_watering": "mdi:stop",
|
||||||
|
"start_watering": "mdi:water"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,17 @@ set_zone_moisture_percent:
|
||||||
min: 0
|
min: 0
|
||||||
max: 100
|
max: 100
|
||||||
unit_of_measurement: "%"
|
unit_of_measurement: "%"
|
||||||
|
start_watering:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
integration: rachio
|
||||||
|
domain: switch
|
||||||
|
fields:
|
||||||
|
duration:
|
||||||
|
example: 15
|
||||||
|
required: false
|
||||||
|
selector:
|
||||||
|
object:
|
||||||
start_multiple_zone_schedule:
|
start_multiple_zone_schedule:
|
||||||
target:
|
target:
|
||||||
entity:
|
entity:
|
||||||
|
|
|
@ -63,6 +63,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"start_watering": {
|
||||||
|
"name": "Start watering",
|
||||||
|
"description": "Start a single zone, a schedule or any number of smart hose timers.",
|
||||||
|
"fields": {
|
||||||
|
"duration": {
|
||||||
|
"name": "Duration",
|
||||||
|
"description": "Number of minutes to run. For sprinkler zones the maximum duration is 3 hours, or 24 hours for smart hose timers. Leave empty for schedules."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"pause_watering": {
|
"pause_watering": {
|
||||||
"name": "Pause watering",
|
"name": "Pause watering",
|
||||||
"description": "Pause any currently running zones or schedules.",
|
"description": "Pause any currently running zones or schedules.",
|
||||||
|
|
|
@ -11,19 +11,28 @@ import voluptuous as vol
|
||||||
from homeassistant.components.switch import SwitchEntity
|
from homeassistant.components.switch import SwitchEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_ID
|
from homeassistant.const import ATTR_ENTITY_ID, ATTR_ID
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, ServiceCall, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
from homeassistant.helpers import (
|
||||||
|
config_validation as cv,
|
||||||
|
device_registry as dr,
|
||||||
|
entity_platform,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
from homeassistant.util.dt import as_timestamp, now, parse_datetime, utc_from_timestamp
|
from homeassistant.util.dt import as_timestamp, now, parse_datetime, utc_from_timestamp
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_MANUAL_RUN_MINS,
|
CONF_MANUAL_RUN_MINS,
|
||||||
DEFAULT_MANUAL_RUN_MINS,
|
DEFAULT_MANUAL_RUN_MINS,
|
||||||
|
DEFAULT_NAME,
|
||||||
DOMAIN as DOMAIN_RACHIO,
|
DOMAIN as DOMAIN_RACHIO,
|
||||||
|
KEY_CONNECTED,
|
||||||
|
KEY_CURRENT_STATUS,
|
||||||
KEY_CUSTOM_CROP,
|
KEY_CUSTOM_CROP,
|
||||||
KEY_CUSTOM_SHADE,
|
KEY_CUSTOM_SHADE,
|
||||||
KEY_CUSTOM_SLOPE,
|
KEY_CUSTOM_SLOPE,
|
||||||
|
@ -36,7 +45,9 @@ from .const import (
|
||||||
KEY_ON,
|
KEY_ON,
|
||||||
KEY_RAIN_DELAY,
|
KEY_RAIN_DELAY,
|
||||||
KEY_RAIN_DELAY_END,
|
KEY_RAIN_DELAY_END,
|
||||||
|
KEY_REPORTED_STATE,
|
||||||
KEY_SCHEDULE_ID,
|
KEY_SCHEDULE_ID,
|
||||||
|
KEY_STATE,
|
||||||
KEY_SUBTYPE,
|
KEY_SUBTYPE,
|
||||||
KEY_SUMMARY,
|
KEY_SUMMARY,
|
||||||
KEY_TYPE,
|
KEY_TYPE,
|
||||||
|
@ -46,6 +57,7 @@ from .const import (
|
||||||
SCHEDULE_TYPE_FLEX,
|
SCHEDULE_TYPE_FLEX,
|
||||||
SERVICE_SET_ZONE_MOISTURE,
|
SERVICE_SET_ZONE_MOISTURE,
|
||||||
SERVICE_START_MULTIPLE_ZONES,
|
SERVICE_START_MULTIPLE_ZONES,
|
||||||
|
SERVICE_START_WATERING,
|
||||||
SIGNAL_RACHIO_CONTROLLER_UPDATE,
|
SIGNAL_RACHIO_CONTROLLER_UPDATE,
|
||||||
SIGNAL_RACHIO_RAIN_DELAY_UPDATE,
|
SIGNAL_RACHIO_RAIN_DELAY_UPDATE,
|
||||||
SIGNAL_RACHIO_SCHEDULE_UPDATE,
|
SIGNAL_RACHIO_SCHEDULE_UPDATE,
|
||||||
|
@ -55,6 +67,7 @@ from .const import (
|
||||||
SLOPE_SLIGHT,
|
SLOPE_SLIGHT,
|
||||||
SLOPE_STEEP,
|
SLOPE_STEEP,
|
||||||
)
|
)
|
||||||
|
from .coordinator import RachioUpdateCoordinator
|
||||||
from .device import RachioPerson
|
from .device import RachioPerson
|
||||||
from .entity import RachioDevice
|
from .entity import RachioDevice
|
||||||
from .webhooks import (
|
from .webhooks import (
|
||||||
|
@ -80,6 +93,7 @@ ATTR_SCHEDULE_ENABLED = "Enabled"
|
||||||
ATTR_SCHEDULE_DURATION = "Duration"
|
ATTR_SCHEDULE_DURATION = "Duration"
|
||||||
ATTR_SCHEDULE_TYPE = "Type"
|
ATTR_SCHEDULE_TYPE = "Type"
|
||||||
ATTR_SORT_ORDER = "sortOrder"
|
ATTR_SORT_ORDER = "sortOrder"
|
||||||
|
ATTR_WATERING_DURATION = "Watering Duration seconds"
|
||||||
ATTR_ZONE_NUMBER = "Zone number"
|
ATTR_ZONE_NUMBER = "Zone number"
|
||||||
ATTR_ZONE_SHADE = "Shade"
|
ATTR_ZONE_SHADE = "Shade"
|
||||||
ATTR_ZONE_SLOPE = "Slope"
|
ATTR_ZONE_SLOPE = "Slope"
|
||||||
|
@ -141,6 +155,19 @@ async def async_setup_entry(
|
||||||
else:
|
else:
|
||||||
raise HomeAssistantError("No matching zones found in given entity_ids")
|
raise HomeAssistantError("No matching zones found in given entity_ids")
|
||||||
|
|
||||||
|
platform = entity_platform.async_get_current_platform()
|
||||||
|
platform.async_register_entity_service(
|
||||||
|
SERVICE_START_WATERING,
|
||||||
|
{
|
||||||
|
vol.Optional(ATTR_DURATION): cv.positive_int,
|
||||||
|
},
|
||||||
|
"turn_on",
|
||||||
|
)
|
||||||
|
|
||||||
|
# If only hose timers on account, none of these services apply
|
||||||
|
if not zone_entities:
|
||||||
|
return
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN_RACHIO,
|
DOMAIN_RACHIO,
|
||||||
SERVICE_START_MULTIPLE_ZONES,
|
SERVICE_START_MULTIPLE_ZONES,
|
||||||
|
@ -176,6 +203,11 @@ def _create_entities(hass: HomeAssistant, config_entry: ConfigEntry) -> list[Ent
|
||||||
RachioSchedule(person, controller, schedule, current_schedule)
|
RachioSchedule(person, controller, schedule, current_schedule)
|
||||||
for schedule in schedules + flex_schedules
|
for schedule in schedules + flex_schedules
|
||||||
)
|
)
|
||||||
|
entities.extend(
|
||||||
|
RachioValve(person, base_station, valve, base_station.coordinator)
|
||||||
|
for base_station in person.base_stations
|
||||||
|
for valve in base_station.coordinator.data.values()
|
||||||
|
)
|
||||||
return entities
|
return entities
|
||||||
|
|
||||||
|
|
||||||
|
@ -246,9 +278,9 @@ class RachioRainDelay(RachioSwitch):
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
_attr_translation_key = "rain_delay"
|
_attr_translation_key = "rain_delay"
|
||||||
|
|
||||||
def __init__(self, controller):
|
def __init__(self, controller) -> None:
|
||||||
"""Set up a Rachio rain delay switch."""
|
"""Set up a Rachio rain delay switch."""
|
||||||
self._cancel_update = None
|
self._cancel_update: CALLBACK_TYPE | None = None
|
||||||
super().__init__(controller)
|
super().__init__(controller)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -324,7 +356,7 @@ class RachioZone(RachioSwitch):
|
||||||
|
|
||||||
_attr_icon = "mdi:water"
|
_attr_icon = "mdi:water"
|
||||||
|
|
||||||
def __init__(self, person, controller, data, current_schedule):
|
def __init__(self, person, controller, data, current_schedule) -> None:
|
||||||
"""Initialize a new Rachio Zone."""
|
"""Initialize a new Rachio Zone."""
|
||||||
self.id = data[KEY_ID]
|
self.id = data[KEY_ID]
|
||||||
self._attr_name = data[KEY_NAME]
|
self._attr_name = data[KEY_NAME]
|
||||||
|
@ -379,11 +411,14 @@ class RachioZone(RachioSwitch):
|
||||||
self.turn_off()
|
self.turn_off()
|
||||||
|
|
||||||
# Start this zone
|
# Start this zone
|
||||||
manual_run_time = timedelta(
|
if ATTR_DURATION in kwargs:
|
||||||
minutes=self._person.config_entry.options.get(
|
manual_run_time = timedelta(minutes=kwargs[ATTR_DURATION])
|
||||||
CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS
|
else:
|
||||||
|
manual_run_time = timedelta(
|
||||||
|
minutes=self._person.config_entry.options.get(
|
||||||
|
CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
# The API limit is 3 hours, and requires an int be passed
|
# The API limit is 3 hours, and requires an int be passed
|
||||||
self._controller.rachio.zone.start(self.zone_id, manual_run_time.seconds)
|
self._controller.rachio.zone.start(self.zone_id, manual_run_time.seconds)
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
|
@ -435,7 +470,7 @@ class RachioZone(RachioSwitch):
|
||||||
class RachioSchedule(RachioSwitch):
|
class RachioSchedule(RachioSwitch):
|
||||||
"""Representation of one fixed schedule on the Rachio Iro."""
|
"""Representation of one fixed schedule on the Rachio Iro."""
|
||||||
|
|
||||||
def __init__(self, person, controller, data, current_schedule):
|
def __init__(self, person, controller, data, current_schedule) -> None:
|
||||||
"""Initialize a new Rachio Schedule."""
|
"""Initialize a new Rachio Schedule."""
|
||||||
self._schedule_id = data[KEY_ID]
|
self._schedule_id = data[KEY_ID]
|
||||||
self._duration = data[KEY_DURATION]
|
self._duration = data[KEY_DURATION]
|
||||||
|
@ -509,3 +544,70 @@ class RachioSchedule(RachioSwitch):
|
||||||
self.hass, SIGNAL_RACHIO_SCHEDULE_UPDATE, self._async_handle_update
|
self.hass, SIGNAL_RACHIO_SCHEDULE_UPDATE, self._async_handle_update
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RachioValve(CoordinatorEntity[RachioUpdateCoordinator], SwitchEntity):
|
||||||
|
"""Representation of one smart hose timer valve."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, person, base, data, coordinator: RachioUpdateCoordinator
|
||||||
|
) -> None:
|
||||||
|
"""Initialize a new smart hose valve."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self._person = person
|
||||||
|
self._base = base
|
||||||
|
self.id = data[KEY_ID]
|
||||||
|
self._attr_name = data[KEY_NAME]
|
||||||
|
self._attr_unique_id = f"{self.id}-valve"
|
||||||
|
self._static_attrs = data[KEY_STATE][KEY_REPORTED_STATE]
|
||||||
|
self._attr_is_on = KEY_CURRENT_STATUS in self._static_attrs
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={
|
||||||
|
(
|
||||||
|
DOMAIN_RACHIO,
|
||||||
|
self.id,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
connections={(dr.CONNECTION_NETWORK_MAC, self._base.mac_address)},
|
||||||
|
manufacturer=DEFAULT_NAME,
|
||||||
|
model="Smart Hose Timer",
|
||||||
|
name=self._attr_name,
|
||||||
|
configuration_url="https://app.rach.io",
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return if the valve is available."""
|
||||||
|
return super().available and self._static_attrs[KEY_CONNECTED]
|
||||||
|
|
||||||
|
def turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn on this valve."""
|
||||||
|
if ATTR_DURATION in kwargs:
|
||||||
|
manual_run_time = timedelta(minutes=kwargs[ATTR_DURATION])
|
||||||
|
else:
|
||||||
|
manual_run_time = timedelta(
|
||||||
|
minutes=self._person.config_entry.options.get(
|
||||||
|
CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._base.start_watering(self.id, manual_run_time.seconds)
|
||||||
|
self._attr_is_on = True
|
||||||
|
self.schedule_update_ha_state(force_refresh=True)
|
||||||
|
_LOGGER.debug("Starting valve %s for %s", self.name, str(manual_run_time))
|
||||||
|
|
||||||
|
def turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn off this valve."""
|
||||||
|
self._base.stop_watering(self.id)
|
||||||
|
self._attr_is_on = False
|
||||||
|
self.schedule_update_ha_state(force_refresh=True)
|
||||||
|
_LOGGER.debug("Stopping watering on valve %s", self.name)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _handle_coordinator_update(self) -> None:
|
||||||
|
"""Handle updated coordinator data."""
|
||||||
|
data = self.coordinator.data[self.id]
|
||||||
|
|
||||||
|
self._static_attrs = data[KEY_STATE][KEY_REPORTED_STATE]
|
||||||
|
self._attr_is_on = KEY_CURRENT_STATUS in self._static_attrs
|
||||||
|
super()._handle_coordinator_update()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue