Centralize rainbird config and add binary sensor platform (#26393)

* Update pyrainbird to version 0.2.0 to fix zone number issue:

- home-assistant/home-assistant/issues/24519
- jbarrancos/pyrainbird/issues/5
- https://community.home-assistant.io/t/rainbird-zone-switches-5-8-dont-correspond/104705

* requirements_all.txt regenerated

* code formatting

* pyrainbird version 0.3.0

* zone id

* rainsensor return state

* updating rainsensor

* new version of pyrainbird

* binary sensor state

* quiet in check format

* is_on instead of state for binary_sensor

* no unit of measurement for binary sensor

* no monitored conditions config

* get keys of dict directly

* removed redundant update of state

* simplified switch

* right states for switch

* raindelay sensor

* raindelay sensor

* binary sensor state

* binary sensor state

* reorganized imports

* doc on public method

* reformatted

* add irrigation service to rain bird, which allows you to set the duration

* rebased on konikvranik and solved some feedback

* add irrigation service to rain bird

* sensor types to constants

* synchronized register service

* patform discovery

* binary sensor as wrapper to sensor

* version 0.4.0

* new config approach

* sensors cleanup

* bypass if no zones found

* platform schema removed

* Change config schema to list of controllers

some small code improvements as suggested in CR:
 - dictionary acces by []
 - just return instead of return False
 - import order
 - no optional parameter name

* some small code improvements as suggested in CR:
 - supported platforms in constant
 - just return instead of return False
 - removed unused constant

* No single controller configuration

Co-Authored-By: Martin Hjelmare <marhje52@kth.se>

* pyrainbird 0.4.1

* individual switch configuration

* imports order

* generate default name out of entity

* trigger time required for controller

* incorporated CR remarks:
- constant fo rzones
- removed SCAN_INTERVAL
- detection of success on initialization
- removed underscore
- refactored if/else
- empty line on end of file
- hass as first parameter

* import of library on top

* refactored

* Update homeassistant/components/rainbird/__init__.py

Co-Authored-By: Martin Hjelmare <marhje52@kth.se>

* validate time and set defaults

* set defaults on right place

* pylint bypass

* iterate over values

* codeowner

* reverted changes:

* irrigation time just as positive integer. Making it complex does make
sense
* zone edfaults fullfiled at runtime. There is no information about
available zones in configuration time.

* codeowners updated

* accept timedelta in irrigation time

* simplified time calculation

* call total_seconds

* irrigation time as seconds.

* simplified schema
This commit is contained in:
Petr Vraník 2019-09-26 11:24:03 +02:00 committed by Martin Hjelmare
parent 82b77c2d29
commit 3efdf29dfa
8 changed files with 226 additions and 93 deletions

View File

@ -226,6 +226,7 @@ homeassistant/components/qld_bushfire/* @exxamalte
homeassistant/components/qnap/* @colinodell homeassistant/components/qnap/* @colinodell
homeassistant/components/quantum_gateway/* @cisasteelersfan homeassistant/components/quantum_gateway/* @cisasteelersfan
homeassistant/components/qwikswitch/* @kellerza homeassistant/components/qwikswitch/* @kellerza
homeassistant/components/rainbird/* @konikvranik
homeassistant/components/raincloud/* @vanstinator homeassistant/components/raincloud/* @vanstinator
homeassistant/components/rainforest_eagle/* @gtdiehl homeassistant/components/rainforest_eagle/* @gtdiehl
homeassistant/components/rainmachine/* @bachya homeassistant/components/rainmachine/* @bachya

View File

@ -1,42 +1,91 @@
"""Support for Rain Bird Irrigation system LNK WiFi Module.""" """Support for Rain Bird Irrigation system LNK WiFi Module."""
import logging import logging
from pyrainbird import RainbirdController
import voluptuous as vol import voluptuous as vol
from homeassistant.components import binary_sensor, sensor, switch
from homeassistant.const import (
CONF_FRIENDLY_NAME,
CONF_HOST,
CONF_PASSWORD,
CONF_TRIGGER_TIME,
)
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_HOST, CONF_PASSWORD
CONF_ZONES = "zones"
SUPPORTED_PLATFORMS = [switch.DOMAIN, sensor.DOMAIN, binary_sensor.DOMAIN]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
RAINBIRD_CONTROLLER = "controller"
DATA_RAINBIRD = "rainbird" DATA_RAINBIRD = "rainbird"
DOMAIN = "rainbird" DOMAIN = "rainbird"
CONFIG_SCHEMA = vol.Schema( SENSOR_TYPE_RAINDELAY = "raindelay"
SENSOR_TYPE_RAINSENSOR = "rainsensor"
# sensor_type [ description, unit, icon ]
SENSOR_TYPES = {
SENSOR_TYPE_RAINSENSOR: ["Rainsensor", None, "mdi:water"],
SENSOR_TYPE_RAINDELAY: ["Raindelay", None, "mdi:water-off"],
}
TRIGGER_TIME_SCHEMA = vol.All(
cv.time_period, cv.positive_timedelta, lambda td: (td.total_seconds() // 60)
)
ZONE_SCHEMA = vol.Schema(
{ {
DOMAIN: vol.Schema( vol.Optional(CONF_FRIENDLY_NAME): cv.string,
{vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string} vol.Optional(CONF_TRIGGER_TIME): TRIGGER_TIME_SCHEMA,
) }
}, )
CONTROLLER_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_TRIGGER_TIME): TRIGGER_TIME_SCHEMA,
vol.Optional(CONF_ZONES): vol.Schema({cv.positive_int: ZONE_SCHEMA}),
}
)
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.Schema(vol.All(cv.ensure_list, [CONTROLLER_SCHEMA]))},
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
def setup(hass, config): def setup(hass, config):
"""Set up the Rain Bird component.""" """Set up the Rain Bird component."""
conf = config[DOMAIN]
server = conf.get(CONF_HOST)
password = conf.get(CONF_PASSWORD)
from pyrainbird import RainbirdController hass.data[DATA_RAINBIRD] = []
success = False
for controller_config in config[DOMAIN]:
success = success or _setup_controller(hass, controller_config, config)
return success
def _setup_controller(hass, controller_config, config):
"""Set up a controller."""
server = controller_config[CONF_HOST]
password = controller_config[CONF_PASSWORD]
controller = RainbirdController(server, password) controller = RainbirdController(server, password)
position = len(hass.data[DATA_RAINBIRD])
_LOGGER.debug("Rain Bird Controller set to: %s", server) try:
controller.get_serial_number()
initial_status = controller.currentIrrigation() except Exception as exc: # pylint: disable=W0703
if initial_status and initial_status["type"] != "CurrentStationsActiveResponse": _LOGGER.error("Unable to setup controller: %s", exc)
_LOGGER.error("Error getting state. Possible configuration issues")
return False return False
hass.data[DATA_RAINBIRD].append(controller)
hass.data[DATA_RAINBIRD] = controller _LOGGER.debug("Rain Bird Controller %d set to: %s", position, server)
for platform in SUPPORTED_PLATFORMS:
discovery.load_platform(
hass,
platform,
DOMAIN,
{RAINBIRD_CONTROLLER: position, **controller_config},
config,
)
return True return True

View File

@ -0,0 +1,64 @@
"""Support for Rain Bird Irrigation system LNK WiFi Module."""
import logging
from pyrainbird import RainbirdController
from homeassistant.components.binary_sensor import BinarySensorDevice
from . import (
DATA_RAINBIRD,
RAINBIRD_CONTROLLER,
SENSOR_TYPE_RAINDELAY,
SENSOR_TYPE_RAINSENSOR,
SENSOR_TYPES,
)
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up a Rain Bird sensor."""
if discovery_info is None:
return
controller = hass.data[DATA_RAINBIRD][discovery_info[RAINBIRD_CONTROLLER]]
add_entities(
[RainBirdSensor(controller, sensor_type) for sensor_type in SENSOR_TYPES], True
)
class RainBirdSensor(BinarySensorDevice):
"""A sensor implementation for Rain Bird device."""
def __init__(self, controller: RainbirdController, sensor_type):
"""Initialize the Rain Bird sensor."""
self._sensor_type = sensor_type
self._controller = controller
self._name = SENSOR_TYPES[self._sensor_type][0]
self._icon = SENSOR_TYPES[self._sensor_type][2]
self._state = None
@property
def is_on(self):
"""Return true if the binary sensor is on."""
return None if self._state is None else bool(self._state)
def update(self):
"""Get the latest data and updates the states."""
_LOGGER.debug("Updating sensor: %s", self._name)
state = None
if self._sensor_type == SENSOR_TYPE_RAINSENSOR:
state = self._controller.get_rain_sensor_state()
elif self._sensor_type == SENSOR_TYPE_RAINDELAY:
state = self._controller.get_rain_delay()
self._state = None if state is None else bool(state)
@property
def name(self):
"""Return the name of this camera."""
return self._name
@property
def icon(self):
"""Return icon."""
return self._icon

View File

@ -3,8 +3,10 @@
"name": "Rainbird", "name": "Rainbird",
"documentation": "https://www.home-assistant.io/components/rainbird", "documentation": "https://www.home-assistant.io/components/rainbird",
"requirements": [ "requirements": [
"pyrainbird==0.2.1" "pyrainbird==0.4.1"
], ],
"dependencies": [], "dependencies": [],
"codeowners": [] "codeowners": [
"@konikvranik"
]
} }

View File

@ -1,44 +1,37 @@
"""Support for Rain Bird Irrigation system LNK WiFi Module.""" """Support for Rain Bird Irrigation system LNK WiFi Module."""
import logging import logging
import voluptuous as vol from pyrainbird import RainbirdController
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_MONITORED_CONDITIONS
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from . import DATA_RAINBIRD from . import (
DATA_RAINBIRD,
RAINBIRD_CONTROLLER,
SENSOR_TYPE_RAINDELAY,
SENSOR_TYPE_RAINSENSOR,
SENSOR_TYPES,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# sensor_type [ description, unit, icon ]
SENSOR_TYPES = {"rainsensor": ["Rainsensor", None, "mdi:water"]}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All(
cv.ensure_list, [vol.In(SENSOR_TYPES)]
)
}
)
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up a Rain Bird sensor.""" """Set up a Rain Bird sensor."""
controller = hass.data[DATA_RAINBIRD]
sensors = [] if discovery_info is None:
for sensor_type in config.get(CONF_MONITORED_CONDITIONS): return
sensors.append(RainBirdSensor(controller, sensor_type))
add_entities(sensors, True) controller = hass.data[DATA_RAINBIRD][discovery_info[RAINBIRD_CONTROLLER]]
add_entities(
[RainBirdSensor(controller, sensor_type) for sensor_type in SENSOR_TYPES], True
)
class RainBirdSensor(Entity): class RainBirdSensor(Entity):
"""A sensor implementation for Rain Bird device.""" """A sensor implementation for Rain Bird device."""
def __init__(self, controller, sensor_type): def __init__(self, controller: RainbirdController, sensor_type):
"""Initialize the Rain Bird sensor.""" """Initialize the Rain Bird sensor."""
self._sensor_type = sensor_type self._sensor_type = sensor_type
self._controller = controller self._controller = controller
@ -55,12 +48,10 @@ class RainBirdSensor(Entity):
def update(self): def update(self):
"""Get the latest data and updates the states.""" """Get the latest data and updates the states."""
_LOGGER.debug("Updating sensor: %s", self._name) _LOGGER.debug("Updating sensor: %s", self._name)
if self._sensor_type == "rainsensor": if self._sensor_type == SENSOR_TYPE_RAINSENSOR:
result = self._controller.currentRainSensorState() self._state = self._controller.get_rain_sensor_state()
if result and result["type"] == "CurrentRainSensorStateResponse": elif self._sensor_type == SENSOR_TYPE_RAINDELAY:
self._state = result["sensorState"] self._state = self._controller.get_rain_delay()
else:
self._state = None
@property @property
def name(self): def name(self):

View File

@ -0,0 +1,9 @@
start_irrigation:
description: Start the irrigation
fields:
entity_id:
description: Name of a single irrigation to turn on
example: 'switch.sprinkler_1'
duration:
description: Duration for this sprinkler to be turned on
example: 1

View File

@ -2,61 +2,85 @@
import logging import logging
from pyrainbird import AvailableStations, RainbirdController
import voluptuous as vol import voluptuous as vol
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.components.switch import SwitchDevice
from homeassistant.const import ( from homeassistant.const import ATTR_ENTITY_ID, CONF_FRIENDLY_NAME, CONF_TRIGGER_TIME
CONF_FRIENDLY_NAME,
CONF_SCAN_INTERVAL,
CONF_SWITCHES,
CONF_TRIGGER_TIME,
CONF_ZONE,
)
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from . import DATA_RAINBIRD from . import CONF_ZONES, DATA_RAINBIRD, DOMAIN, RAINBIRD_CONTROLLER
DOMAIN = "rainbird"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ATTR_DURATION = "duration"
SERVICE_START_IRRIGATION = "start_irrigation"
SERVICE_SCHEMA_IRRIGATION = vol.Schema(
{ {
vol.Required(CONF_SWITCHES, default={}): vol.Schema( vol.Required(ATTR_ENTITY_ID): cv.entity_id,
{ vol.Required(ATTR_DURATION): vol.All(vol.Coerce(float), vol.Range(min=0)),
cv.string: {
vol.Optional(CONF_FRIENDLY_NAME): cv.string,
vol.Required(CONF_ZONE): cv.string,
vol.Required(CONF_TRIGGER_TIME): cv.string,
vol.Optional(CONF_SCAN_INTERVAL): cv.string,
}
}
)
} }
) )
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up Rain Bird switches over a Rain Bird controller.""" """Set up Rain Bird switches over a Rain Bird controller."""
controller = hass.data[DATA_RAINBIRD]
if discovery_info is None:
return
controller: RainbirdController = hass.data[DATA_RAINBIRD][
discovery_info[RAINBIRD_CONTROLLER]
]
available_stations: AvailableStations = controller.get_available_stations()
if not (available_stations and available_stations.stations):
return
devices = [] devices = []
for dev_id, switch in config.get(CONF_SWITCHES).items(): for zone in range(1, available_stations.stations.count + 1):
devices.append(RainBirdSwitch(controller, switch, dev_id)) if available_stations.stations.active(zone):
zone_config = discovery_info.get(CONF_ZONES, {}).get(zone, {})
time = zone_config.get(CONF_TRIGGER_TIME, discovery_info[CONF_TRIGGER_TIME])
name = zone_config.get(CONF_FRIENDLY_NAME)
devices.append(
RainBirdSwitch(
controller,
zone,
time,
name if name else "Sprinkler {}".format(zone),
)
)
add_entities(devices, True) add_entities(devices, True)
def start_irrigation(service):
entity_id = service.data[ATTR_ENTITY_ID]
duration = service.data[ATTR_DURATION]
for device in devices:
if device.entity_id == entity_id:
device.turn_on(duration=duration)
hass.services.register(
DOMAIN,
SERVICE_START_IRRIGATION,
start_irrigation,
schema=SERVICE_SCHEMA_IRRIGATION,
)
class RainBirdSwitch(SwitchDevice): class RainBirdSwitch(SwitchDevice):
"""Representation of a Rain Bird switch.""" """Representation of a Rain Bird switch."""
def __init__(self, rb, dev, dev_id): def __init__(self, controller: RainbirdController, zone, time, name):
"""Initialize a Rain Bird Switch Device.""" """Initialize a Rain Bird Switch Device."""
self._rainbird = rb self._rainbird = controller
self._devid = dev_id self._zone = zone
self._zone = int(dev.get(CONF_ZONE)) self._name = name
self._name = dev.get(CONF_FRIENDLY_NAME, f"Sprinkler {self._zone}")
self._state = None self._state = None
self._duration = dev.get(CONF_TRIGGER_TIME) self._duration = time
self._attributes = {"duration": self._duration, "zone": self._zone} self._attributes = {ATTR_DURATION: self._duration, "zone": self._zone}
@property @property
def device_state_attributes(self): def device_state_attributes(self):
@ -70,27 +94,20 @@ class RainBirdSwitch(SwitchDevice):
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
"""Turn the switch on.""" """Turn the switch on."""
response = self._rainbird.startIrrigation(int(self._zone), int(self._duration)) if self._rainbird.irrigate_zone(
if response and response["type"] == "AcknowledgeResponse": int(self._zone),
int(kwargs[ATTR_DURATION] if ATTR_DURATION in kwargs else self._duration),
):
self._state = True self._state = True
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
"""Turn the switch off.""" """Turn the switch off."""
response = self._rainbird.stopIrrigation() if self._rainbird.stop_irrigation():
if response and response["type"] == "AcknowledgeResponse":
self._state = False self._state = False
def get_device_status(self):
"""Get the status of the switch from Rain Bird Controller."""
response = self._rainbird.currentIrrigation()
if response is None:
return None
if isinstance(response, dict) and "sprinklers" in response:
return response["sprinklers"][self._zone]
def update(self): def update(self):
"""Update switch status.""" """Update switch status."""
self._state = self.get_device_status() self._state = self._rainbird.get_zone_state(self._zone)
@property @property
def is_on(self): def is_on(self):

View File

@ -1396,7 +1396,7 @@ pyqwikswitch==0.93
pyrail==0.0.3 pyrail==0.0.3
# homeassistant.components.rainbird # homeassistant.components.rainbird
pyrainbird==0.2.1 pyrainbird==0.4.1
# homeassistant.components.recswitch # homeassistant.components.recswitch
pyrecswitch==1.0.2 pyrecswitch==1.0.2