mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Adds useful attributes to RainMachine programs and zones (#14087)
* Starting to add attributes * All attributes added to programs * Basic zone attributes in place * Added advanced properties for zones * Working to move common logic into component + dispatcher * We shouldn't calculate the MAC with every entity * Small fixes * Small adjustments * Owner-requested changes * Restart * Restart part 2 * Added ID attribute to each switch * Collaborator-requested changes
This commit is contained in:
parent
9c7523d7b0
commit
f516cc7dc6
@ -5,13 +5,14 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/rainmachine/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers import config_validation as cv, discovery
|
||||
from homeassistant.const import (
|
||||
CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_SWITCHES)
|
||||
ATTR_ATTRIBUTION, CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL,
|
||||
CONF_SWITCHES)
|
||||
from homeassistant.helpers import config_validation as cv, discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['regenmaschine==0.4.1']
|
||||
|
||||
@ -26,11 +27,11 @@ NOTIFICATION_TITLE = 'RainMachine Component Setup'
|
||||
CONF_ZONE_RUN_TIME = 'zone_run_time'
|
||||
|
||||
DEFAULT_ATTRIBUTION = 'Data provided by Green Electronics LLC'
|
||||
DEFAULT_ICON = 'mdi:water'
|
||||
DEFAULT_PORT = 8080
|
||||
DEFAULT_SSL = True
|
||||
|
||||
MIN_SCAN_TIME = timedelta(seconds=1)
|
||||
MIN_SCAN_TIME_FORCED = timedelta(milliseconds=100)
|
||||
PROGRAM_UPDATE_TOPIC = '{0}_program_update'.format(DOMAIN)
|
||||
|
||||
SWITCH_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_ZONE_RUN_TIME):
|
||||
@ -68,8 +69,7 @@ def setup(hass, config):
|
||||
auth = Authenticator.create_local(
|
||||
ip_address, password, port=port, https=ssl)
|
||||
client = Client(auth)
|
||||
mac = client.provision.wifi()['macAddress']
|
||||
hass.data[DATA_RAINMACHINE] = (client, mac)
|
||||
hass.data[DATA_RAINMACHINE] = RainMachine(client)
|
||||
except (HTTPError, ConnectTimeout, UnboundLocalError) as exc_info:
|
||||
_LOGGER.error('An error occurred: %s', str(exc_info))
|
||||
hass.components.persistent_notification.create(
|
||||
@ -87,3 +87,46 @@ def setup(hass, config):
|
||||
_LOGGER.debug('Setup complete')
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class RainMachine(object):
|
||||
"""Define a generic RainMachine object."""
|
||||
|
||||
def __init__(self, client):
|
||||
"""Initialize."""
|
||||
self.client = client
|
||||
self.device_mac = self.client.provision.wifi()['macAddress']
|
||||
|
||||
|
||||
class RainMachineEntity(Entity):
|
||||
"""Define a generic RainMachine entity."""
|
||||
|
||||
def __init__(self,
|
||||
rainmachine,
|
||||
rainmachine_type,
|
||||
rainmachine_entity_id,
|
||||
icon=DEFAULT_ICON):
|
||||
"""Initialize."""
|
||||
self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
|
||||
self._icon = icon
|
||||
self._rainmachine_type = rainmachine_type
|
||||
self._rainmachine_entity_id = rainmachine_entity_id
|
||||
self.rainmachine = rainmachine
|
||||
|
||||
@property
|
||||
def device_state_attributes(self) -> dict:
|
||||
"""Return the state attributes."""
|
||||
return self._attrs
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""Return the icon."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique, HASS-friendly identifier for this entity."""
|
||||
return '{0}_{1}_{2}'.format(
|
||||
self.rainmachine.device_mac.replace(
|
||||
':', ''), self._rainmachine_type,
|
||||
self._rainmachine_entity_id)
|
||||
|
@ -1,26 +1,118 @@
|
||||
"""Implements a RainMachine sprinkler controller for Home Assistant."""
|
||||
"""
|
||||
This component provides support for RainMachine programs and zones.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/switch.rainmachine/
|
||||
"""
|
||||
|
||||
from logging import getLogger
|
||||
|
||||
from homeassistant.components.rainmachine import (
|
||||
CONF_ZONE_RUN_TIME, DATA_RAINMACHINE, DEFAULT_ATTRIBUTION, MIN_SCAN_TIME,
|
||||
MIN_SCAN_TIME_FORCED)
|
||||
CONF_ZONE_RUN_TIME, DATA_RAINMACHINE, PROGRAM_UPDATE_TOPIC,
|
||||
RainMachineEntity)
|
||||
from homeassistant.const import ATTR_ID
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect, dispatcher_send)
|
||||
|
||||
DEPENDENCIES = ['rainmachine']
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
|
||||
ATTR_AREA = 'area'
|
||||
ATTR_CS_ON = 'cs_on'
|
||||
ATTR_CURRENT_CYCLE = 'current_cycle'
|
||||
ATTR_CYCLES = 'cycles'
|
||||
ATTR_TOTAL_DURATION = 'total_duration'
|
||||
ATTR_DELAY = 'delay'
|
||||
ATTR_DELAY_ON = 'delay_on'
|
||||
ATTR_FIELD_CAPACITY = 'field_capacity'
|
||||
ATTR_NO_CYCLES = 'number_of_cycles'
|
||||
ATTR_PRECIP_RATE = 'sprinkler_head_precipitation_rate'
|
||||
ATTR_RESTRICTIONS = 'restrictions'
|
||||
ATTR_SLOPE = 'slope'
|
||||
ATTR_SOAK = 'soak'
|
||||
ATTR_SOIL_TYPE = 'soil_type'
|
||||
ATTR_SPRINKLER_TYPE = 'sprinkler_head_type'
|
||||
ATTR_STATUS = 'status'
|
||||
ATTR_SUN_EXPOSURE = 'sun_exposure'
|
||||
ATTR_VEGETATION_TYPE = 'vegetation_type'
|
||||
ATTR_ZONES = 'zones'
|
||||
|
||||
DEFAULT_ZONE_RUN = 60 * 10
|
||||
|
||||
DAYS = [
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
'Sunday'
|
||||
]
|
||||
|
||||
PROGRAM_STATUS_MAP = {
|
||||
0: 'Not Running',
|
||||
1: 'Running',
|
||||
2: 'Queued'
|
||||
}
|
||||
|
||||
SOIL_TYPE_MAP = {
|
||||
0: 'Not Set',
|
||||
1: 'Clay Loam',
|
||||
2: 'Silty Clay',
|
||||
3: 'Clay',
|
||||
4: 'Loam',
|
||||
5: 'Sandy Loam',
|
||||
6: 'Loamy Sand',
|
||||
7: 'Sand',
|
||||
8: 'Sandy Clay',
|
||||
9: 'Silt Loam',
|
||||
10: 'Silt',
|
||||
99: 'Other'
|
||||
}
|
||||
|
||||
SLOPE_TYPE_MAP = {
|
||||
0: 'Not Set',
|
||||
1: 'Flat',
|
||||
2: 'Moderate',
|
||||
3: 'High',
|
||||
4: 'Very High',
|
||||
99: 'Other'
|
||||
}
|
||||
|
||||
SPRINKLER_TYPE_MAP = {
|
||||
0: 'Not Set',
|
||||
1: 'Popup Spray',
|
||||
2: 'Rotors',
|
||||
3: 'Surface Drip',
|
||||
4: 'Bubblers',
|
||||
99: 'Other'
|
||||
}
|
||||
|
||||
SUN_EXPOSURE_MAP = {
|
||||
0: 'Not Set',
|
||||
1: 'Full Sun',
|
||||
2: 'Partial Shade',
|
||||
3: 'Full Shade'
|
||||
}
|
||||
|
||||
VEGETATION_MAP = {
|
||||
0: 'Not Set',
|
||||
1: 'Not Set',
|
||||
2: 'Grass',
|
||||
3: 'Fruit Trees',
|
||||
4: 'Flowers',
|
||||
5: 'Vegetables',
|
||||
6: 'Citrus',
|
||||
7: 'Bushes',
|
||||
8: 'Xeriscape',
|
||||
99: 'Other'
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set this component up under its platform."""
|
||||
"""Set up the RainMachine Switch platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
@ -28,181 +120,196 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
zone_run_time = discovery_info.get(CONF_ZONE_RUN_TIME, DEFAULT_ZONE_RUN)
|
||||
|
||||
client, device_mac = hass.data.get(DATA_RAINMACHINE)
|
||||
rainmachine = hass.data[DATA_RAINMACHINE]
|
||||
|
||||
entities = []
|
||||
for program in client.programs.all().get('programs', {}):
|
||||
for program in rainmachine.client.programs.all().get('programs', {}):
|
||||
if not program.get('active'):
|
||||
continue
|
||||
|
||||
_LOGGER.debug('Adding program: %s', program)
|
||||
entities.append(
|
||||
RainMachineProgram(client, device_mac, program))
|
||||
entities.append(RainMachineProgram(rainmachine, program))
|
||||
|
||||
for zone in client.zones.all().get('zones', {}):
|
||||
for zone in rainmachine.client.zones.all().get('zones', {}):
|
||||
if not zone.get('active'):
|
||||
continue
|
||||
|
||||
_LOGGER.debug('Adding zone: %s', zone)
|
||||
entities.append(
|
||||
RainMachineZone(client, device_mac, zone,
|
||||
zone_run_time))
|
||||
entities.append(RainMachineZone(rainmachine, zone, zone_run_time))
|
||||
|
||||
add_devices(entities, True)
|
||||
|
||||
|
||||
class RainMachineEntity(SwitchDevice):
|
||||
class RainMachineSwitch(RainMachineEntity, SwitchDevice):
|
||||
"""A class to represent a generic RainMachine entity."""
|
||||
|
||||
def __init__(self, client, device_mac, entity_json):
|
||||
def __init__(self, rainmachine, rainmachine_type, obj):
|
||||
"""Initialize a generic RainMachine entity."""
|
||||
self._api_type = 'remote' if client.auth.using_remote_api else 'local'
|
||||
self._client = client
|
||||
self._entity_json = entity_json
|
||||
self._obj = obj
|
||||
self._type = rainmachine_type
|
||||
|
||||
self.device_mac = device_mac
|
||||
|
||||
self._attrs = {
|
||||
ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION
|
||||
}
|
||||
|
||||
@property
|
||||
def device_state_attributes(self) -> dict:
|
||||
"""Return the state attributes."""
|
||||
return self._attrs
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""Return the icon."""
|
||||
return 'mdi:water'
|
||||
super().__init__(rainmachine, rainmachine_type, obj.get('uid'))
|
||||
|
||||
@property
|
||||
def is_enabled(self) -> bool:
|
||||
"""Return whether the entity is enabled."""
|
||||
return self._entity_json.get('active')
|
||||
|
||||
@property
|
||||
def rainmachine_entity_id(self) -> int:
|
||||
"""Return the RainMachine ID for this entity."""
|
||||
return self._entity_json.get('uid')
|
||||
return self._obj.get('active')
|
||||
|
||||
|
||||
class RainMachineProgram(RainMachineEntity):
|
||||
class RainMachineProgram(RainMachineSwitch):
|
||||
"""A RainMachine program."""
|
||||
|
||||
def __init__(self, rainmachine, obj):
|
||||
"""Initialize."""
|
||||
super().__init__(rainmachine, 'program', obj)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return whether the program is running."""
|
||||
return bool(self._entity_json.get('status'))
|
||||
return bool(self._obj.get('status'))
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the program."""
|
||||
return 'Program: {0}'.format(self._entity_json.get('name'))
|
||||
return 'Program: {0}'.format(self._obj.get('name'))
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique, HASS-friendly identifier for this entity."""
|
||||
return '{0}_program_{1}'.format(
|
||||
self.device_mac.replace(':', ''), self.rainmachine_entity_id)
|
||||
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']]
|
||||
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
"""Turn the program off."""
|
||||
import regenmaschine.exceptions as exceptions
|
||||
from regenmaschine.exceptions import HTTPError
|
||||
|
||||
try:
|
||||
self._client.programs.stop(self.rainmachine_entity_id)
|
||||
except exceptions.BrokenAPICall:
|
||||
_LOGGER.error('programs.stop currently broken in remote API')
|
||||
except exceptions.HTTPError as exc_info:
|
||||
self.rainmachine.client.programs.stop(self._rainmachine_entity_id)
|
||||
dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC)
|
||||
except HTTPError as exc_info:
|
||||
_LOGGER.error('Unable to turn off program "%s"', self.unique_id)
|
||||
_LOGGER.debug(exc_info)
|
||||
|
||||
def turn_on(self, **kwargs) -> None:
|
||||
"""Turn the program on."""
|
||||
import regenmaschine.exceptions as exceptions
|
||||
from regenmaschine.exceptions import HTTPError
|
||||
|
||||
try:
|
||||
self._client.programs.start(self.rainmachine_entity_id)
|
||||
except exceptions.BrokenAPICall:
|
||||
_LOGGER.error('programs.start currently broken in remote API')
|
||||
except exceptions.HTTPError as exc_info:
|
||||
self.rainmachine.client.programs.start(self._rainmachine_entity_id)
|
||||
dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC)
|
||||
except HTTPError as exc_info:
|
||||
_LOGGER.error('Unable to turn on program "%s"', self.unique_id)
|
||||
_LOGGER.debug(exc_info)
|
||||
|
||||
@Throttle(MIN_SCAN_TIME, MIN_SCAN_TIME_FORCED)
|
||||
def update(self) -> None:
|
||||
"""Update info for the program."""
|
||||
import regenmaschine.exceptions as exceptions
|
||||
from regenmaschine.exceptions import HTTPError
|
||||
|
||||
try:
|
||||
self._entity_json = self._client.programs.get(
|
||||
self.rainmachine_entity_id)
|
||||
except exceptions.HTTPError as exc_info:
|
||||
self._obj = self.rainmachine.client.programs.get(
|
||||
self._rainmachine_entity_id)
|
||||
|
||||
self._attrs.update({
|
||||
ATTR_ID: self._obj['uid'],
|
||||
ATTR_CS_ON: self._obj.get('cs_on'),
|
||||
ATTR_CYCLES: self._obj.get('cycles'),
|
||||
ATTR_DELAY: self._obj.get('delay'),
|
||||
ATTR_DELAY_ON: self._obj.get('delay_on'),
|
||||
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 HTTPError as exc_info:
|
||||
_LOGGER.error('Unable to update info for program "%s"',
|
||||
self.unique_id)
|
||||
_LOGGER.debug(exc_info)
|
||||
|
||||
|
||||
class RainMachineZone(RainMachineEntity):
|
||||
class RainMachineZone(RainMachineSwitch):
|
||||
"""A RainMachine zone."""
|
||||
|
||||
def __init__(self, client, device_mac, zone_json,
|
||||
zone_run_time):
|
||||
def __init__(self, rainmachine, obj, zone_run_time):
|
||||
"""Initialize a RainMachine zone."""
|
||||
super().__init__(client, device_mac, zone_json)
|
||||
super().__init__(rainmachine, 'zone', obj)
|
||||
|
||||
self._properties_json = {}
|
||||
self._run_time = zone_run_time
|
||||
self._attrs.update({
|
||||
ATTR_CYCLES: self._entity_json.get('noOfCycles'),
|
||||
ATTR_TOTAL_DURATION: self._entity_json.get('userDuration')
|
||||
})
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return whether the zone is running."""
|
||||
return bool(self._entity_json.get('state'))
|
||||
return bool(self._obj.get('state'))
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the zone."""
|
||||
return 'Zone: {0}'.format(self._entity_json.get('name'))
|
||||
return 'Zone: {0}'.format(self._obj.get('name'))
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique, HASS-friendly identifier for this entity."""
|
||||
return '{0}_zone_{1}'.format(
|
||||
self.device_mac.replace(':', ''), self.rainmachine_entity_id)
|
||||
@callback
|
||||
def _program_updated(self):
|
||||
"""Update state, trigger updates."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(self.hass, PROGRAM_UPDATE_TOPIC,
|
||||
self._program_updated)
|
||||
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
"""Turn the zone off."""
|
||||
import regenmaschine.exceptions as exceptions
|
||||
from regenmaschine.exceptions import HTTPError
|
||||
|
||||
try:
|
||||
self._client.zones.stop(self.rainmachine_entity_id)
|
||||
except exceptions.HTTPError as exc_info:
|
||||
self.rainmachine.client.zones.stop(self._rainmachine_entity_id)
|
||||
except HTTPError as exc_info:
|
||||
_LOGGER.error('Unable to turn off zone "%s"', self.unique_id)
|
||||
_LOGGER.debug(exc_info)
|
||||
|
||||
def turn_on(self, **kwargs) -> None:
|
||||
"""Turn the zone on."""
|
||||
import regenmaschine.exceptions as exceptions
|
||||
from regenmaschine.exceptions import HTTPError
|
||||
|
||||
try:
|
||||
self._client.zones.start(self.rainmachine_entity_id,
|
||||
self._run_time)
|
||||
except exceptions.HTTPError as exc_info:
|
||||
self.rainmachine.client.zones.start(self._rainmachine_entity_id,
|
||||
self._run_time)
|
||||
except HTTPError as exc_info:
|
||||
_LOGGER.error('Unable to turn on zone "%s"', self.unique_id)
|
||||
_LOGGER.debug(exc_info)
|
||||
|
||||
@Throttle(MIN_SCAN_TIME, MIN_SCAN_TIME_FORCED)
|
||||
def update(self) -> None:
|
||||
"""Update info for the zone."""
|
||||
import regenmaschine.exceptions as exceptions
|
||||
from regenmaschine.exceptions import HTTPError
|
||||
|
||||
try:
|
||||
self._entity_json = self._client.zones.get(
|
||||
self.rainmachine_entity_id)
|
||||
except exceptions.HTTPError as exc_info:
|
||||
self._obj = self.rainmachine.client.zones.get(
|
||||
self._rainmachine_entity_id)
|
||||
|
||||
self._properties_json = self.rainmachine.client.zones.get(
|
||||
self._rainmachine_entity_id, properties=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[self._properties_json.get('slope')],
|
||||
ATTR_SOIL_TYPE:
|
||||
SOIL_TYPE_MAP[self._properties_json.get('sun')],
|
||||
ATTR_SPRINKLER_TYPE:
|
||||
SPRINKLER_TYPE_MAP[self._properties_json.get('group_id')],
|
||||
ATTR_SUN_EXPOSURE:
|
||||
SUN_EXPOSURE_MAP[self._properties_json.get('sun')],
|
||||
ATTR_VEGETATION_TYPE:
|
||||
VEGETATION_MAP[self._obj.get('type')],
|
||||
})
|
||||
except HTTPError as exc_info:
|
||||
_LOGGER.error('Unable to update info for zone "%s"',
|
||||
self.unique_id)
|
||||
_LOGGER.debug(exc_info)
|
||||
|
Loading…
x
Reference in New Issue
Block a user