Refactor RainMachine switch platform (#31148)
* Import constants sanely * Linting * Rename data constants for consistency * Refactor RainMachine switch platform * Comments * Cleanup * Refactor switch and sensor API calls to be separate * Linting * Make sure zones are updated in appropriate service calls * Correctly decrement * Linting * Don't do weird inheritance * Ensure service calls update data properly * Docstring * Docstring * Errors can be logged without string conversion * Code review commentspull/31149/head
parent
37d1cdc4cb
commit
4c4f726323
|
@ -27,23 +27,25 @@ from homeassistant.helpers.service import verify_domain_control
|
|||
from .config_flow import configured_instances
|
||||
from .const import (
|
||||
DATA_CLIENT,
|
||||
DATA_PROGRAMS,
|
||||
DATA_PROVISION_SETTINGS,
|
||||
DATA_RESTRICTIONS_CURRENT,
|
||||
DATA_RESTRICTIONS_UNIVERSAL,
|
||||
DATA_ZONES,
|
||||
DATA_ZONES_DETAILS,
|
||||
DEFAULT_PORT,
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
DEFAULT_SSL,
|
||||
DOMAIN,
|
||||
PROVISION_SETTINGS,
|
||||
RESTRICTIONS_CURRENT,
|
||||
RESTRICTIONS_UNIVERSAL,
|
||||
PROGRAM_UPDATE_TOPIC,
|
||||
SENSOR_UPDATE_TOPIC,
|
||||
ZONE_UPDATE_TOPIC,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_LISTENER = "listener"
|
||||
|
||||
PROGRAM_UPDATE_TOPIC = f"{DOMAIN}_program_update"
|
||||
SENSOR_UPDATE_TOPIC = f"{DOMAIN}_data_update"
|
||||
ZONE_UPDATE_TOPIC = f"{DOMAIN}_zone_update"
|
||||
|
||||
CONF_CONTROLLERS = "controllers"
|
||||
CONF_PROGRAM_ID = "program_id"
|
||||
CONF_SECONDS = "seconds"
|
||||
|
@ -150,6 +152,9 @@ async def async_setup_entry(hass, config_entry):
|
|||
_LOGGER.error("An error occurred: %s", err)
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
# Update the data object, which at this point (prior to any sensors registering
|
||||
# "interest" in the API), will focus on grabbing the latest program and zone data:
|
||||
await rainmachine.async_update()
|
||||
hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = rainmachine
|
||||
|
||||
for component in ("binary_sensor", "sensor", "switch"):
|
||||
|
@ -161,37 +166,37 @@ async def async_setup_entry(hass, config_entry):
|
|||
async def disable_program(call):
|
||||
"""Disable a program."""
|
||||
await rainmachine.client.programs.disable(call.data[CONF_PROGRAM_ID])
|
||||
async_dispatcher_send(hass, PROGRAM_UPDATE_TOPIC)
|
||||
await rainmachine.async_update_programs_and_zones()
|
||||
|
||||
@_verify_domain_control
|
||||
async def disable_zone(call):
|
||||
"""Disable a zone."""
|
||||
await rainmachine.client.zones.disable(call.data[CONF_ZONE_ID])
|
||||
async_dispatcher_send(hass, ZONE_UPDATE_TOPIC)
|
||||
await rainmachine.async_update_programs_and_zones()
|
||||
|
||||
@_verify_domain_control
|
||||
async def enable_program(call):
|
||||
"""Enable a program."""
|
||||
await rainmachine.client.programs.enable(call.data[CONF_PROGRAM_ID])
|
||||
async_dispatcher_send(hass, PROGRAM_UPDATE_TOPIC)
|
||||
await rainmachine.async_update_programs_and_zones()
|
||||
|
||||
@_verify_domain_control
|
||||
async def enable_zone(call):
|
||||
"""Enable a zone."""
|
||||
await rainmachine.client.zones.enable(call.data[CONF_ZONE_ID])
|
||||
async_dispatcher_send(hass, ZONE_UPDATE_TOPIC)
|
||||
await rainmachine.async_update_programs_and_zones()
|
||||
|
||||
@_verify_domain_control
|
||||
async def pause_watering(call):
|
||||
"""Pause watering for a set number of seconds."""
|
||||
await rainmachine.client.watering.pause_all(call.data[CONF_SECONDS])
|
||||
async_dispatcher_send(hass, PROGRAM_UPDATE_TOPIC)
|
||||
await rainmachine.async_update_programs_and_zones()
|
||||
|
||||
@_verify_domain_control
|
||||
async def start_program(call):
|
||||
"""Start a particular program."""
|
||||
await rainmachine.client.programs.start(call.data[CONF_PROGRAM_ID])
|
||||
async_dispatcher_send(hass, PROGRAM_UPDATE_TOPIC)
|
||||
await rainmachine.async_update_programs_and_zones()
|
||||
|
||||
@_verify_domain_control
|
||||
async def start_zone(call):
|
||||
|
@ -199,31 +204,31 @@ async def async_setup_entry(hass, config_entry):
|
|||
await rainmachine.client.zones.start(
|
||||
call.data[CONF_ZONE_ID], call.data[CONF_ZONE_RUN_TIME]
|
||||
)
|
||||
async_dispatcher_send(hass, ZONE_UPDATE_TOPIC)
|
||||
await rainmachine.async_update_programs_and_zones()
|
||||
|
||||
@_verify_domain_control
|
||||
async def stop_all(call):
|
||||
"""Stop all watering."""
|
||||
await rainmachine.client.watering.stop_all()
|
||||
async_dispatcher_send(hass, PROGRAM_UPDATE_TOPIC)
|
||||
await rainmachine.async_update_programs_and_zones()
|
||||
|
||||
@_verify_domain_control
|
||||
async def stop_program(call):
|
||||
"""Stop a program."""
|
||||
await rainmachine.client.programs.stop(call.data[CONF_PROGRAM_ID])
|
||||
async_dispatcher_send(hass, PROGRAM_UPDATE_TOPIC)
|
||||
await rainmachine.async_update_programs_and_zones()
|
||||
|
||||
@_verify_domain_control
|
||||
async def stop_zone(call):
|
||||
"""Stop a zone."""
|
||||
await rainmachine.client.zones.stop(call.data[CONF_ZONE_ID])
|
||||
async_dispatcher_send(hass, ZONE_UPDATE_TOPIC)
|
||||
await rainmachine.async_update_programs_and_zones()
|
||||
|
||||
@_verify_domain_control
|
||||
async def unpause_watering(call):
|
||||
"""Unpause watering."""
|
||||
await rainmachine.client.watering.unpause_all()
|
||||
async_dispatcher_send(hass, PROGRAM_UPDATE_TOPIC)
|
||||
await rainmachine.async_update_programs_and_zones()
|
||||
|
||||
for service, method, schema in [
|
||||
("disable_program", disable_program, SERVICE_ALTER_PROGRAM),
|
||||
|
@ -265,7 +270,7 @@ class RainMachine:
|
|||
|
||||
def __init__(self, hass, client, default_zone_runtime, scan_interval):
|
||||
"""Initialize."""
|
||||
self._async_unsub_dispatcher_connect = None
|
||||
self._async_cancel_time_interval_listener = None
|
||||
self._scan_interval_seconds = scan_interval
|
||||
self.client = client
|
||||
self.data = {}
|
||||
|
@ -274,48 +279,58 @@ class RainMachine:
|
|||
self.hass = hass
|
||||
|
||||
self._api_category_count = {
|
||||
PROVISION_SETTINGS: 0,
|
||||
RESTRICTIONS_CURRENT: 0,
|
||||
RESTRICTIONS_UNIVERSAL: 0,
|
||||
DATA_PROVISION_SETTINGS: 0,
|
||||
DATA_RESTRICTIONS_CURRENT: 0,
|
||||
DATA_RESTRICTIONS_UNIVERSAL: 0,
|
||||
}
|
||||
self._api_category_locks = {
|
||||
PROVISION_SETTINGS: asyncio.Lock(),
|
||||
RESTRICTIONS_CURRENT: asyncio.Lock(),
|
||||
RESTRICTIONS_UNIVERSAL: asyncio.Lock(),
|
||||
DATA_PROVISION_SETTINGS: asyncio.Lock(),
|
||||
DATA_RESTRICTIONS_CURRENT: asyncio.Lock(),
|
||||
DATA_RESTRICTIONS_UNIVERSAL: asyncio.Lock(),
|
||||
}
|
||||
|
||||
async def _async_fetch_from_api(self, api_category):
|
||||
"""Execute the appropriate coroutine to fetch particular data from the API."""
|
||||
if api_category == PROVISION_SETTINGS:
|
||||
data = await self.client.provisioning.settings()
|
||||
elif api_category == RESTRICTIONS_CURRENT:
|
||||
data = await self.client.restrictions.current()
|
||||
elif api_category == RESTRICTIONS_UNIVERSAL:
|
||||
data = await self.client.restrictions.universal()
|
||||
|
||||
return data
|
||||
|
||||
async def _async_update_listener_action(self, now):
|
||||
"""Define an async_track_time_interval action to update data."""
|
||||
await self.async_update()
|
||||
|
||||
@callback
|
||||
def async_deregister_api_interest(self, api_category):
|
||||
def async_deregister_sensor_api_interest(self, api_category):
|
||||
"""Decrement the number of entities with data needs from an API category."""
|
||||
# If this deregistration should leave us with no registration at all, remove the
|
||||
# time interval:
|
||||
if sum(self._api_category_count.values()) == 0:
|
||||
if self._async_unsub_dispatcher_connect:
|
||||
self._async_unsub_dispatcher_connect()
|
||||
self._async_unsub_dispatcher_connect = None
|
||||
if self._async_cancel_time_interval_listener:
|
||||
self._async_cancel_time_interval_listener()
|
||||
self._async_cancel_time_interval_listener = None
|
||||
return
|
||||
self._api_category_count[api_category] += 1
|
||||
|
||||
async def async_register_api_interest(self, api_category):
|
||||
self._api_category_count[api_category] -= 1
|
||||
|
||||
async def async_fetch_from_api(self, api_category):
|
||||
"""Execute the appropriate coroutine to fetch particular data from the API."""
|
||||
if api_category == DATA_PROGRAMS:
|
||||
data = await self.client.programs.all(include_inactive=True)
|
||||
elif api_category == DATA_PROVISION_SETTINGS:
|
||||
data = await self.client.provisioning.settings()
|
||||
elif api_category == DATA_RESTRICTIONS_CURRENT:
|
||||
data = await self.client.restrictions.current()
|
||||
elif api_category == DATA_RESTRICTIONS_UNIVERSAL:
|
||||
data = await self.client.restrictions.universal()
|
||||
elif api_category == DATA_ZONES:
|
||||
data = await self.client.zones.all(include_inactive=True)
|
||||
elif api_category == DATA_ZONES_DETAILS:
|
||||
# This API call needs to be separate from the DATA_ZONES one above because,
|
||||
# maddeningly, the DATA_ZONES_DETAILS API call doesn't include the current
|
||||
# state of the zone:
|
||||
data = await self.client.zones.all(details=True, include_inactive=True)
|
||||
|
||||
self.data[api_category] = data
|
||||
|
||||
async def async_register_sensor_api_interest(self, api_category):
|
||||
"""Increment the number of entities with data needs from an API category."""
|
||||
# If this is the first registration we have, start a time interval:
|
||||
if not self._async_unsub_dispatcher_connect:
|
||||
self._async_unsub_dispatcher_connect = async_track_time_interval(
|
||||
if not self._async_cancel_time_interval_listener:
|
||||
self._async_cancel_time_interval_listener = async_track_time_interval(
|
||||
self.hass,
|
||||
self._async_update_listener_action,
|
||||
timedelta(seconds=self._scan_interval_seconds),
|
||||
|
@ -323,19 +338,27 @@ class RainMachine:
|
|||
|
||||
self._api_category_count[api_category] += 1
|
||||
|
||||
# Lock API updates in case multiple entities are trying to call the same API
|
||||
# endpoint at once:
|
||||
# If a sensor registers interest in a particular API call and the data doesn't
|
||||
# exist for it yet, make the API call and grab the data:
|
||||
async with self._api_category_locks[api_category]:
|
||||
if api_category not in self.data:
|
||||
self.data[api_category] = await self._async_fetch_from_api(api_category)
|
||||
await self.async_fetch_from_api(api_category)
|
||||
|
||||
async def async_update(self):
|
||||
"""Update all RainMachine data."""
|
||||
tasks = [self.async_update_programs_and_zones(), self.async_update_sensors()]
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
async def async_update_sensors(self):
|
||||
"""Update sensor/binary sensor data."""
|
||||
_LOGGER.debug("Updating sensor data for RainMachine")
|
||||
|
||||
# Fetch an API category if there is at least one interested entity:
|
||||
tasks = {}
|
||||
for category, count in self._api_category_count.items():
|
||||
if count == 0:
|
||||
continue
|
||||
tasks[category] = self._async_fetch_from_api(category)
|
||||
tasks[category] = self.async_fetch_from_api(category)
|
||||
|
||||
results = await asyncio.gather(*tasks.values(), return_exceptions=True)
|
||||
for api_category, result in zip(tasks, results):
|
||||
|
@ -344,10 +367,37 @@ class RainMachine:
|
|||
"There was an error while updating %s: %s", api_category, result
|
||||
)
|
||||
continue
|
||||
self.data[api_category] = result
|
||||
|
||||
async_dispatcher_send(self.hass, SENSOR_UPDATE_TOPIC)
|
||||
|
||||
async def async_update_programs_and_zones(self):
|
||||
"""Update program and zone data.
|
||||
|
||||
Program and zone updates always go together because of how linked they are:
|
||||
programs affect zones and certain combinations of zones affect programs.
|
||||
|
||||
Note that this call does not take into account interested entities when making
|
||||
the API calls; we make the reasonable assumption that switches will always be
|
||||
enabled.
|
||||
"""
|
||||
_LOGGER.debug("Updating program and zone data for RainMachine")
|
||||
|
||||
tasks = {
|
||||
DATA_PROGRAMS: self.async_fetch_from_api(DATA_PROGRAMS),
|
||||
DATA_ZONES: self.async_fetch_from_api(DATA_ZONES),
|
||||
DATA_ZONES_DETAILS: self.async_fetch_from_api(DATA_ZONES_DETAILS),
|
||||
}
|
||||
|
||||
results = await asyncio.gather(*tasks.values(), return_exceptions=True)
|
||||
for api_category, result in zip(tasks, results):
|
||||
if isinstance(result, RainMachineError):
|
||||
_LOGGER.error(
|
||||
"There was an error while updating %s: %s", api_category, result
|
||||
)
|
||||
|
||||
async_dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC)
|
||||
async_dispatcher_send(self.hass, ZONE_UPDATE_TOPIC)
|
||||
|
||||
|
||||
class RainMachineEntity(Entity):
|
||||
"""Define a generic RainMachine entity."""
|
||||
|
@ -389,6 +439,16 @@ class RainMachineEntity(Entity):
|
|||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Disable polling."""
|
||||
return False
|
||||
|
||||
@callback
|
||||
def _update_state(self):
|
||||
"""Update the state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Disconnect dispatcher listener when removed."""
|
||||
for handler in self._dispatcher_handlers:
|
||||
|
|
|
@ -2,17 +2,16 @@
|
|||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from . import (
|
||||
from . import RainMachineEntity
|
||||
from .const import (
|
||||
DATA_CLIENT,
|
||||
DATA_PROVISION_SETTINGS,
|
||||
DATA_RESTRICTIONS_CURRENT,
|
||||
DATA_RESTRICTIONS_UNIVERSAL,
|
||||
DOMAIN as RAINMACHINE_DOMAIN,
|
||||
PROVISION_SETTINGS,
|
||||
RESTRICTIONS_CURRENT,
|
||||
RESTRICTIONS_UNIVERSAL,
|
||||
SENSOR_UPDATE_TOPIC,
|
||||
RainMachineEntity,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -28,35 +27,45 @@ TYPE_RAINSENSOR = "rainsensor"
|
|||
TYPE_WEEKDAY = "weekday"
|
||||
|
||||
BINARY_SENSORS = {
|
||||
TYPE_FLOW_SENSOR: ("Flow Sensor", "mdi:water-pump", True, PROVISION_SETTINGS),
|
||||
TYPE_FREEZE: ("Freeze Restrictions", "mdi:cancel", True, RESTRICTIONS_CURRENT),
|
||||
TYPE_FLOW_SENSOR: ("Flow Sensor", "mdi:water-pump", True, DATA_PROVISION_SETTINGS),
|
||||
TYPE_FREEZE: ("Freeze Restrictions", "mdi:cancel", True, DATA_RESTRICTIONS_CURRENT),
|
||||
TYPE_FREEZE_PROTECTION: (
|
||||
"Freeze Protection",
|
||||
"mdi:weather-snowy",
|
||||
True,
|
||||
RESTRICTIONS_UNIVERSAL,
|
||||
DATA_RESTRICTIONS_UNIVERSAL,
|
||||
),
|
||||
TYPE_HOT_DAYS: (
|
||||
"Extra Water on Hot Days",
|
||||
"mdi:thermometer-lines",
|
||||
True,
|
||||
RESTRICTIONS_UNIVERSAL,
|
||||
DATA_RESTRICTIONS_UNIVERSAL,
|
||||
),
|
||||
TYPE_HOURLY: ("Hourly Restrictions", "mdi:cancel", False, RESTRICTIONS_CURRENT),
|
||||
TYPE_MONTH: ("Month Restrictions", "mdi:cancel", False, RESTRICTIONS_CURRENT),
|
||||
TYPE_HOURLY: (
|
||||
"Hourly Restrictions",
|
||||
"mdi:cancel",
|
||||
False,
|
||||
DATA_RESTRICTIONS_CURRENT,
|
||||
),
|
||||
TYPE_MONTH: ("Month Restrictions", "mdi:cancel", False, DATA_RESTRICTIONS_CURRENT),
|
||||
TYPE_RAINDELAY: (
|
||||
"Rain Delay Restrictions",
|
||||
"mdi:cancel",
|
||||
False,
|
||||
RESTRICTIONS_CURRENT,
|
||||
DATA_RESTRICTIONS_CURRENT,
|
||||
),
|
||||
TYPE_RAINSENSOR: (
|
||||
"Rain Sensor Restrictions",
|
||||
"mdi:cancel",
|
||||
False,
|
||||
RESTRICTIONS_CURRENT,
|
||||
DATA_RESTRICTIONS_CURRENT,
|
||||
),
|
||||
TYPE_WEEKDAY: (
|
||||
"Weekday Restrictions",
|
||||
"mdi:cancel",
|
||||
False,
|
||||
DATA_RESTRICTIONS_CURRENT,
|
||||
),
|
||||
TYPE_WEEKDAY: ("Weekday Restrictions", "mdi:cancel", False, RESTRICTIONS_CURRENT),
|
||||
}
|
||||
|
||||
|
||||
|
@ -107,11 +116,6 @@ class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice):
|
|||
"""Return the status of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Disable polling."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique, Home Assistant friendly identifier for this entity."""
|
||||
|
@ -121,46 +125,40 @@ class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice):
|
|||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
|
||||
@callback
|
||||
def update():
|
||||
"""Update the state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
self._dispatcher_handlers.append(
|
||||
async_dispatcher_connect(self.hass, SENSOR_UPDATE_TOPIC, update)
|
||||
async_dispatcher_connect(self.hass, SENSOR_UPDATE_TOPIC, self._update_state)
|
||||
)
|
||||
await self.rainmachine.async_register_api_interest(self._api_category)
|
||||
await self.rainmachine.async_register_sensor_api_interest(self._api_category)
|
||||
await self.async_update()
|
||||
|
||||
async def async_update(self):
|
||||
"""Update the state."""
|
||||
if self._sensor_type == TYPE_FLOW_SENSOR:
|
||||
self._state = self.rainmachine.data[PROVISION_SETTINGS]["system"].get(
|
||||
self._state = self.rainmachine.data[DATA_PROVISION_SETTINGS]["system"].get(
|
||||
"useFlowSensor"
|
||||
)
|
||||
elif self._sensor_type == TYPE_FREEZE:
|
||||
self._state = self.rainmachine.data[RESTRICTIONS_CURRENT]["freeze"]
|
||||
self._state = self.rainmachine.data[DATA_RESTRICTIONS_CURRENT]["freeze"]
|
||||
elif self._sensor_type == TYPE_FREEZE_PROTECTION:
|
||||
self._state = self.rainmachine.data[RESTRICTIONS_UNIVERSAL][
|
||||
self._state = self.rainmachine.data[DATA_RESTRICTIONS_UNIVERSAL][
|
||||
"freezeProtectEnabled"
|
||||
]
|
||||
elif self._sensor_type == TYPE_HOT_DAYS:
|
||||
self._state = self.rainmachine.data[RESTRICTIONS_UNIVERSAL][
|
||||
self._state = self.rainmachine.data[DATA_RESTRICTIONS_UNIVERSAL][
|
||||
"hotDaysExtraWatering"
|
||||
]
|
||||
elif self._sensor_type == TYPE_HOURLY:
|
||||
self._state = self.rainmachine.data[RESTRICTIONS_CURRENT]["hourly"]
|
||||
self._state = self.rainmachine.data[DATA_RESTRICTIONS_CURRENT]["hourly"]
|
||||
elif self._sensor_type == TYPE_MONTH:
|
||||
self._state = self.rainmachine.data[RESTRICTIONS_CURRENT]["month"]
|
||||
self._state = self.rainmachine.data[DATA_RESTRICTIONS_CURRENT]["month"]
|
||||
elif self._sensor_type == TYPE_RAINDELAY:
|
||||
self._state = self.rainmachine.data[RESTRICTIONS_CURRENT]["rainDelay"]
|
||||
self._state = self.rainmachine.data[DATA_RESTRICTIONS_CURRENT]["rainDelay"]
|
||||
elif self._sensor_type == TYPE_RAINSENSOR:
|
||||
self._state = self.rainmachine.data[RESTRICTIONS_CURRENT]["rainSensor"]
|
||||
self._state = self.rainmachine.data[DATA_RESTRICTIONS_CURRENT]["rainSensor"]
|
||||
elif self._sensor_type == TYPE_WEEKDAY:
|
||||
self._state = self.rainmachine.data[RESTRICTIONS_CURRENT]["weekDay"]
|
||||
self._state = self.rainmachine.data[DATA_RESTRICTIONS_CURRENT]["weekDay"]
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Disconnect dispatcher listeners and deregister API interest."""
|
||||
super().async_will_remove_from_hass()
|
||||
self.rainmachine.async_deregister_api_interest(self._api_category)
|
||||
self.rainmachine.async_deregister_sensor_api_interest(self._api_category)
|
||||
|
|
|
@ -7,13 +7,17 @@ LOGGER = logging.getLogger(__package__)
|
|||
DOMAIN = "rainmachine"
|
||||
|
||||
DATA_CLIENT = "client"
|
||||
DATA_PROGRAMS = "programs"
|
||||
DATA_PROVISION_SETTINGS = "provision.settings"
|
||||
DATA_RESTRICTIONS_CURRENT = "restrictions.current"
|
||||
DATA_RESTRICTIONS_UNIVERSAL = "restrictions.universal"
|
||||
DATA_ZONES = "zones"
|
||||
DATA_ZONES_DETAILS = "zones_details"
|
||||
|
||||
DEFAULT_PORT = 8080
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(seconds=60)
|
||||
DEFAULT_SSL = True
|
||||
|
||||
PROVISION_SETTINGS = "provision.settings"
|
||||
RESTRICTIONS_CURRENT = "restrictions.current"
|
||||
RESTRICTIONS_UNIVERSAL = "restrictions.universal"
|
||||
|
||||
TOPIC_UPDATE = "update_{0}"
|
||||
PROGRAM_UPDATE_TOPIC = f"{DOMAIN}_program_update"
|
||||
SENSOR_UPDATE_TOPIC = f"{DOMAIN}_data_update"
|
||||
ZONE_UPDATE_TOPIC = f"{DOMAIN}_zone_update"
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
"""This platform provides support for sensor data from RainMachine."""
|
||||
import logging
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from . import (
|
||||
from . import RainMachineEntity
|
||||
from .const import (
|
||||
DATA_CLIENT,
|
||||
DATA_PROVISION_SETTINGS,
|
||||
DATA_RESTRICTIONS_UNIVERSAL,
|
||||
DOMAIN as RAINMACHINE_DOMAIN,
|
||||
PROVISION_SETTINGS,
|
||||
RESTRICTIONS_UNIVERSAL,
|
||||
SENSOR_UPDATE_TOPIC,
|
||||
RainMachineEntity,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -28,7 +27,7 @@ SENSORS = {
|
|||
"clicks/m^3",
|
||||
None,
|
||||
False,
|
||||
PROVISION_SETTINGS,
|
||||
DATA_PROVISION_SETTINGS,
|
||||
),
|
||||
TYPE_FLOW_SENSOR_CONSUMED_LITERS: (
|
||||
"Flow Sensor Consumed Liters",
|
||||
|
@ -36,7 +35,7 @@ SENSORS = {
|
|||
"liter",
|
||||
None,
|
||||
False,
|
||||
PROVISION_SETTINGS,
|
||||
DATA_PROVISION_SETTINGS,
|
||||
),
|
||||
TYPE_FLOW_SENSOR_START_INDEX: (
|
||||
"Flow Sensor Start Index",
|
||||
|
@ -44,7 +43,7 @@ SENSORS = {
|
|||
"index",
|
||||
None,
|
||||
False,
|
||||
PROVISION_SETTINGS,
|
||||
DATA_PROVISION_SETTINGS,
|
||||
),
|
||||
TYPE_FLOW_SENSOR_WATERING_CLICKS: (
|
||||
"Flow Sensor Clicks",
|
||||
|
@ -52,7 +51,7 @@ SENSORS = {
|
|||
"clicks",
|
||||
None,
|
||||
False,
|
||||
PROVISION_SETTINGS,
|
||||
DATA_PROVISION_SETTINGS,
|
||||
),
|
||||
TYPE_FREEZE_TEMP: (
|
||||
"Freeze Protect Temperature",
|
||||
|
@ -60,7 +59,7 @@ SENSORS = {
|
|||
"°C",
|
||||
"temperature",
|
||||
True,
|
||||
RESTRICTIONS_UNIVERSAL,
|
||||
DATA_RESTRICTIONS_UNIVERSAL,
|
||||
),
|
||||
}
|
||||
|
||||
|
@ -124,11 +123,6 @@ class RainMachineSensor(RainMachineEntity):
|
|||
"""Return the icon."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Disable polling."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
"""Return the name of the entity."""
|
||||
|
@ -148,50 +142,44 @@ class RainMachineSensor(RainMachineEntity):
|
|||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
|
||||
@callback
|
||||
def update():
|
||||
"""Update the state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
self._dispatcher_handlers.append(
|
||||
async_dispatcher_connect(self.hass, SENSOR_UPDATE_TOPIC, update)
|
||||
async_dispatcher_connect(self.hass, SENSOR_UPDATE_TOPIC, self._update_state)
|
||||
)
|
||||
await self.rainmachine.async_register_api_interest(self._api_category)
|
||||
await self.rainmachine.async_register_sensor_api_interest(self._api_category)
|
||||
await self.async_update()
|
||||
|
||||
async def async_update(self):
|
||||
"""Update the sensor's state."""
|
||||
if self._sensor_type == TYPE_FLOW_SENSOR_CLICK_M3:
|
||||
self._state = self.rainmachine.data[PROVISION_SETTINGS]["system"].get(
|
||||
self._state = self.rainmachine.data[DATA_PROVISION_SETTINGS]["system"].get(
|
||||
"flowSensorClicksPerCubicMeter"
|
||||
)
|
||||
elif self._sensor_type == TYPE_FLOW_SENSOR_CONSUMED_LITERS:
|
||||
clicks = self.rainmachine.data[PROVISION_SETTINGS]["system"].get(
|
||||
clicks = self.rainmachine.data[DATA_PROVISION_SETTINGS]["system"].get(
|
||||
"flowSensorWateringClicks"
|
||||
)
|
||||
clicks_per_m3 = self.rainmachine.data[PROVISION_SETTINGS]["system"].get(
|
||||
"flowSensorClicksPerCubicMeter"
|
||||
)
|
||||
clicks_per_m3 = self.rainmachine.data[DATA_PROVISION_SETTINGS][
|
||||
"system"
|
||||
].get("flowSensorClicksPerCubicMeter")
|
||||
|
||||
if clicks and clicks_per_m3:
|
||||
self._state = (clicks * 1000) / clicks_per_m3
|
||||
else:
|
||||
self._state = None
|
||||
elif self._sensor_type == TYPE_FLOW_SENSOR_START_INDEX:
|
||||
self._state = self.rainmachine.data[PROVISION_SETTINGS]["system"].get(
|
||||
self._state = self.rainmachine.data[DATA_PROVISION_SETTINGS]["system"].get(
|
||||
"flowSensorStartIndex"
|
||||
)
|
||||
elif self._sensor_type == TYPE_FLOW_SENSOR_WATERING_CLICKS:
|
||||
self._state = self.rainmachine.data[PROVISION_SETTINGS]["system"].get(
|
||||
self._state = self.rainmachine.data[DATA_PROVISION_SETTINGS]["system"].get(
|
||||
"flowSensorWateringClicks"
|
||||
)
|
||||
elif self._sensor_type == TYPE_FREEZE_TEMP:
|
||||
self._state = self.rainmachine.data[RESTRICTIONS_UNIVERSAL][
|
||||
self._state = self.rainmachine.data[DATA_RESTRICTIONS_UNIVERSAL][
|
||||
"freezeProtectTemp"
|
||||
]
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Disconnect dispatcher listeners and deregister API interest."""
|
||||
super().async_will_remove_from_hass()
|
||||
self.rainmachine.async_deregister_api_interest(self._api_category)
|
||||
self.rainmachine.async_deregister_sensor_api_interest(self._api_category)
|
||||
|
|
|
@ -6,18 +6,17 @@ from regenmaschine.errors import RequestError
|
|||
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
from homeassistant.const import ATTR_ID
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from . import (
|
||||
from . import RainMachineEntity
|
||||
from .const import (
|
||||
DATA_CLIENT,
|
||||
DATA_PROGRAMS,
|
||||
DATA_ZONES,
|
||||
DATA_ZONES_DETAILS,
|
||||
DOMAIN as RAINMACHINE_DOMAIN,
|
||||
PROGRAM_UPDATE_TOPIC,
|
||||
ZONE_UPDATE_TOPIC,
|
||||
RainMachineEntity,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -94,22 +93,19 @@ VEGETATION_MAP = {
|
|||
99: "Other",
|
||||
}
|
||||
|
||||
SWITCH_TYPE_PROGRAM = "program"
|
||||
SWITCH_TYPE_ZONE = "zone"
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
"""Set up RainMachine switches based on a config entry."""
|
||||
rainmachine = hass.data[RAINMACHINE_DOMAIN][DATA_CLIENT][entry.entry_id]
|
||||
|
||||
entities = []
|
||||
|
||||
programs = await rainmachine.client.programs.all(include_inactive=True)
|
||||
for program in programs:
|
||||
for program in rainmachine.data[DATA_PROGRAMS]:
|
||||
entities.append(RainMachineProgram(rainmachine, program))
|
||||
|
||||
zones = await rainmachine.client.zones.all(include_inactive=True)
|
||||
for zone in zones:
|
||||
entities.append(
|
||||
RainMachineZone(rainmachine, zone, rainmachine.default_zone_runtime)
|
||||
)
|
||||
for zone in rainmachine.data[DATA_ZONES]:
|
||||
entities.append(RainMachineZone(rainmachine, zone))
|
||||
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
@ -117,25 +113,31 @@ async def async_setup_entry(hass, entry, async_add_entities):
|
|||
class RainMachineSwitch(RainMachineEntity, SwitchDevice):
|
||||
"""A class to represent a generic RainMachine switch."""
|
||||
|
||||
def __init__(self, rainmachine, switch_type, obj):
|
||||
def __init__(self, rainmachine, switch_data):
|
||||
"""Initialize a generic RainMachine switch."""
|
||||
super().__init__(rainmachine)
|
||||
|
||||
self._name = obj["name"]
|
||||
self._obj = obj
|
||||
self._rainmachine_entity_id = obj["uid"]
|
||||
self._switch_type = switch_type
|
||||
self._is_on = False
|
||||
self._name = switch_data["name"]
|
||||
self._switch_data = switch_data
|
||||
self._rainmachine_entity_id = switch_data["uid"]
|
||||
self._switch_type = None
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return bool(self._obj.get("active"))
|
||||
return self._switch_data["active"]
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""Return the icon."""
|
||||
return "mdi:water"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return whether the program is running."""
|
||||
return self._is_on
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique, Home Assistant friendly identifier for this entity."""
|
||||
|
@ -145,173 +147,156 @@ class RainMachineSwitch(RainMachineEntity, SwitchDevice):
|
|||
self._rainmachine_entity_id,
|
||||
)
|
||||
|
||||
@callback
|
||||
def _program_updated(self):
|
||||
"""Update state, trigger updates."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
async def _async_run_switch_coroutine(self, api_coro) -> None:
|
||||
"""Run a coroutine to toggle the switch."""
|
||||
try:
|
||||
resp = await api_coro
|
||||
except RequestError as err:
|
||||
_LOGGER.error(
|
||||
'Error while toggling %s "%s": %s',
|
||||
self._switch_type,
|
||||
self.unique_id,
|
||||
err,
|
||||
)
|
||||
return
|
||||
|
||||
if resp["statusCode"] != 0:
|
||||
_LOGGER.error(
|
||||
'Error while toggling %s "%s": %s',
|
||||
self._switch_type,
|
||||
self.unique_id,
|
||||
resp["message"],
|
||||
)
|
||||
return
|
||||
|
||||
self.hass.async_create_task(self.rainmachine.async_update_programs_and_zones())
|
||||
|
||||
|
||||
class RainMachineProgram(RainMachineSwitch):
|
||||
"""A RainMachine program."""
|
||||
|
||||
def __init__(self, rainmachine, obj):
|
||||
def __init__(self, rainmachine, switch_data):
|
||||
"""Initialize a generic RainMachine switch."""
|
||||
super().__init__(rainmachine, "program", obj)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return whether the program is running."""
|
||||
return bool(self._obj.get("status"))
|
||||
super().__init__(rainmachine, switch_data)
|
||||
self._switch_type = SWITCH_TYPE_PROGRAM
|
||||
|
||||
@property
|
||||
def zones(self) -> list:
|
||||
"""Return a list of active zones associated with this program."""
|
||||
return [z for z in self._obj["wateringTimes"] if z["active"]]
|
||||
return [z for z in self._switch_data["wateringTimes"] if z["active"]]
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
self._dispatcher_handlers.append(
|
||||
async_dispatcher_connect(
|
||||
self.hass, PROGRAM_UPDATE_TOPIC, self._program_updated
|
||||
self.hass, PROGRAM_UPDATE_TOPIC, self._update_state
|
||||
)
|
||||
)
|
||||
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
"""Turn the program off."""
|
||||
|
||||
try:
|
||||
await self.rainmachine.client.programs.stop(self._rainmachine_entity_id)
|
||||
async_dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC)
|
||||
except RequestError as err:
|
||||
_LOGGER.error(
|
||||
'Unable to turn off program "%s": %s', self.unique_id, str(err)
|
||||
)
|
||||
await self._async_run_switch_coroutine(
|
||||
self.rainmachine.client.programs.stop(self._rainmachine_entity_id)
|
||||
)
|
||||
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
"""Turn the program on."""
|
||||
|
||||
try:
|
||||
await self.rainmachine.client.programs.start(self._rainmachine_entity_id)
|
||||
async_dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC)
|
||||
except RequestError as err:
|
||||
_LOGGER.error(
|
||||
'Unable to turn on program "%s": %s', self.unique_id, str(err)
|
||||
)
|
||||
await self._async_run_switch_coroutine(
|
||||
self.rainmachine.client.programs.start(self._rainmachine_entity_id)
|
||||
)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update info for the program."""
|
||||
[self._switch_data] = [
|
||||
p
|
||||
for p in self.rainmachine.data[DATA_PROGRAMS]
|
||||
if p["uid"] == self._rainmachine_entity_id
|
||||
]
|
||||
|
||||
self._is_on = bool(self._switch_data["status"])
|
||||
|
||||
try:
|
||||
self._obj = await self.rainmachine.client.programs.get(
|
||||
self._rainmachine_entity_id
|
||||
)
|
||||
next_run = datetime.strptime(
|
||||
"{0} {1}".format(
|
||||
self._switch_data["nextRun"], self._switch_data["startTime"]
|
||||
),
|
||||
"%Y-%m-%d %H:%M",
|
||||
).isoformat()
|
||||
except ValueError:
|
||||
next_run = None
|
||||
|
||||
try:
|
||||
next_run = datetime.strptime(
|
||||
"{0} {1}".format(self._obj["nextRun"], self._obj["startTime"]),
|
||||
"%Y-%m-%d %H:%M",
|
||||
).isoformat()
|
||||
except ValueError:
|
||||
next_run = None
|
||||
|
||||
self._attrs.update(
|
||||
{
|
||||
ATTR_ID: self._obj["uid"],
|
||||
ATTR_NEXT_RUN: next_run,
|
||||
ATTR_SOAK: self._obj.get("soak"),
|
||||
ATTR_STATUS: PROGRAM_STATUS_MAP[self._obj.get("status")],
|
||||
ATTR_ZONES: ", ".join(z["name"] for z in self.zones),
|
||||
}
|
||||
)
|
||||
except RequestError as err:
|
||||
_LOGGER.error(
|
||||
'Unable to update info for program "%s": %s', self.unique_id, str(err)
|
||||
)
|
||||
self._attrs.update(
|
||||
{
|
||||
ATTR_ID: self._switch_data["uid"],
|
||||
ATTR_NEXT_RUN: next_run,
|
||||
ATTR_SOAK: self._switch_data.get("soak"),
|
||||
ATTR_STATUS: PROGRAM_STATUS_MAP[self._switch_data["status"]],
|
||||
ATTR_ZONES: ", ".join(z["name"] for z in self.zones),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class RainMachineZone(RainMachineSwitch):
|
||||
"""A RainMachine zone."""
|
||||
|
||||
def __init__(self, rainmachine, obj, zone_run_time):
|
||||
def __init__(self, rainmachine, switch_data):
|
||||
"""Initialize a RainMachine zone."""
|
||||
super().__init__(rainmachine, "zone", obj)
|
||||
|
||||
self._properties_json = {}
|
||||
self._run_time = zone_run_time
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return whether the zone is running."""
|
||||
return bool(self._obj.get("state"))
|
||||
super().__init__(rainmachine, switch_data)
|
||||
self._switch_type = SWITCH_TYPE_ZONE
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
self._dispatcher_handlers.append(
|
||||
async_dispatcher_connect(
|
||||
self.hass, PROGRAM_UPDATE_TOPIC, self._program_updated
|
||||
self.hass, PROGRAM_UPDATE_TOPIC, self._update_state
|
||||
)
|
||||
)
|
||||
self._dispatcher_handlers.append(
|
||||
async_dispatcher_connect(
|
||||
self.hass, ZONE_UPDATE_TOPIC, self._program_updated
|
||||
)
|
||||
async_dispatcher_connect(self.hass, ZONE_UPDATE_TOPIC, self._update_state)
|
||||
)
|
||||
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
"""Turn the zone off."""
|
||||
|
||||
try:
|
||||
await self.rainmachine.client.zones.stop(self._rainmachine_entity_id)
|
||||
except RequestError as err:
|
||||
_LOGGER.error('Unable to turn off zone "%s": %s', self.unique_id, str(err))
|
||||
await self._async_run_switch_coroutine(
|
||||
self.rainmachine.client.zones.stop(self._rainmachine_entity_id)
|
||||
)
|
||||
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
"""Turn the zone on."""
|
||||
|
||||
try:
|
||||
await self.rainmachine.client.zones.start(
|
||||
self._rainmachine_entity_id, self._run_time
|
||||
await self._async_run_switch_coroutine(
|
||||
self.rainmachine.client.zones.start(
|
||||
self._rainmachine_entity_id, self.rainmachine.default_zone_runtime
|
||||
)
|
||||
except RequestError as err:
|
||||
_LOGGER.error('Unable to turn on zone "%s": %s', self.unique_id, str(err))
|
||||
)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update info for the zone."""
|
||||
[self._switch_data] = [
|
||||
z
|
||||
for z in self.rainmachine.data[DATA_ZONES]
|
||||
if z["uid"] == self._rainmachine_entity_id
|
||||
]
|
||||
[details] = [
|
||||
z
|
||||
for z in self.rainmachine.data[DATA_ZONES_DETAILS]
|
||||
if z["uid"] == self._rainmachine_entity_id
|
||||
]
|
||||
|
||||
try:
|
||||
self._obj = await self.rainmachine.client.zones.get(
|
||||
self._rainmachine_entity_id
|
||||
)
|
||||
self._is_on = bool(self._switch_data["state"])
|
||||
|
||||
self._properties_json = await self.rainmachine.client.zones.get(
|
||||
self._rainmachine_entity_id, details=True
|
||||
)
|
||||
|
||||
self._attrs.update(
|
||||
{
|
||||
ATTR_ID: self._obj["uid"],
|
||||
ATTR_AREA: self._properties_json.get("waterSense").get("area"),
|
||||
ATTR_CURRENT_CYCLE: self._obj.get("cycle"),
|
||||
ATTR_FIELD_CAPACITY: self._properties_json.get("waterSense").get(
|
||||
"fieldCapacity"
|
||||
),
|
||||
ATTR_NO_CYCLES: self._obj.get("noOfCycles"),
|
||||
ATTR_PRECIP_RATE: self._properties_json.get("waterSense").get(
|
||||
"precipitationRate"
|
||||
),
|
||||
ATTR_RESTRICTIONS: self._obj.get("restriction"),
|
||||
ATTR_SLOPE: SLOPE_TYPE_MAP.get(self._properties_json.get("slope")),
|
||||
ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(self._properties_json.get("sun")),
|
||||
ATTR_SPRINKLER_TYPE: SPRINKLER_TYPE_MAP.get(
|
||||
self._properties_json.get("group_id")
|
||||
),
|
||||
ATTR_SUN_EXPOSURE: SUN_EXPOSURE_MAP.get(
|
||||
self._properties_json.get("sun")
|
||||
),
|
||||
ATTR_VEGETATION_TYPE: VEGETATION_MAP.get(self._obj.get("type")),
|
||||
}
|
||||
)
|
||||
except RequestError as err:
|
||||
_LOGGER.error(
|
||||
'Unable to update info for zone "%s": %s', self.unique_id, str(err)
|
||||
)
|
||||
self._attrs.update(
|
||||
{
|
||||
ATTR_ID: self._switch_data["uid"],
|
||||
ATTR_AREA: details.get("waterSense").get("area"),
|
||||
ATTR_CURRENT_CYCLE: self._switch_data.get("cycle"),
|
||||
ATTR_FIELD_CAPACITY: details.get("waterSense").get("fieldCapacity"),
|
||||
ATTR_NO_CYCLES: self._switch_data.get("noOfCycles"),
|
||||
ATTR_PRECIP_RATE: details.get("waterSense").get("precipitationRate"),
|
||||
ATTR_RESTRICTIONS: self._switch_data.get("restriction"),
|
||||
ATTR_SLOPE: SLOPE_TYPE_MAP.get(details.get("slope")),
|
||||
ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(details.get("sun")),
|
||||
ATTR_SPRINKLER_TYPE: SPRINKLER_TYPE_MAP.get(details.get("group_id")),
|
||||
ATTR_SUN_EXPOSURE: SUN_EXPOSURE_MAP.get(details.get("sun")),
|
||||
ATTR_VEGETATION_TYPE: VEGETATION_MAP.get(self._switch_data.get("type")),
|
||||
}
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue