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/quantum_gateway/* @cisasteelersfan
homeassistant/components/qwikswitch/* @kellerza
homeassistant/components/rainbird/* @konikvranik
homeassistant/components/raincloud/* @vanstinator
homeassistant/components/rainforest_eagle/* @gtdiehl
homeassistant/components/rainmachine/* @bachya

View File

@ -1,42 +1,91 @@
"""Support for Rain Bird Irrigation system LNK WiFi Module."""
import logging
from pyrainbird import RainbirdController
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
from homeassistant.const import CONF_HOST, CONF_PASSWORD
CONF_ZONES = "zones"
SUPPORTED_PLATFORMS = [switch.DOMAIN, sensor.DOMAIN, binary_sensor.DOMAIN]
_LOGGER = logging.getLogger(__name__)
RAINBIRD_CONTROLLER = "controller"
DATA_RAINBIRD = "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.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string}
)
},
vol.Optional(CONF_FRIENDLY_NAME): 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,
)
def setup(hass, config):
"""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)
_LOGGER.debug("Rain Bird Controller set to: %s", server)
initial_status = controller.currentIrrigation()
if initial_status and initial_status["type"] != "CurrentStationsActiveResponse":
_LOGGER.error("Error getting state. Possible configuration issues")
position = len(hass.data[DATA_RAINBIRD])
try:
controller.get_serial_number()
except Exception as exc: # pylint: disable=W0703
_LOGGER.error("Unable to setup controller: %s", exc)
return False
hass.data[DATA_RAINBIRD] = controller
hass.data[DATA_RAINBIRD].append(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

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",
"documentation": "https://www.home-assistant.io/components/rainbird",
"requirements": [
"pyrainbird==0.2.1"
"pyrainbird==0.4.1"
],
"dependencies": [],
"codeowners": []
"codeowners": [
"@konikvranik"
]
}

View File

@ -1,44 +1,37 @@
"""Support for Rain Bird Irrigation system LNK WiFi Module."""
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 . import DATA_RAINBIRD
from . import (
DATA_RAINBIRD,
RAINBIRD_CONTROLLER,
SENSOR_TYPE_RAINDELAY,
SENSOR_TYPE_RAINSENSOR,
SENSOR_TYPES,
)
_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):
"""Set up a Rain Bird sensor."""
controller = hass.data[DATA_RAINBIRD]
sensors = []
for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
sensors.append(RainBirdSensor(controller, sensor_type))
if discovery_info is None:
return
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):
"""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."""
self._sensor_type = sensor_type
self._controller = controller
@ -55,12 +48,10 @@ class RainBirdSensor(Entity):
def update(self):
"""Get the latest data and updates the states."""
_LOGGER.debug("Updating sensor: %s", self._name)
if self._sensor_type == "rainsensor":
result = self._controller.currentRainSensorState()
if result and result["type"] == "CurrentRainSensorStateResponse":
self._state = result["sensorState"]
else:
self._state = None
if self._sensor_type == SENSOR_TYPE_RAINSENSOR:
self._state = self._controller.get_rain_sensor_state()
elif self._sensor_type == SENSOR_TYPE_RAINDELAY:
self._state = self._controller.get_rain_delay()
@property
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
from pyrainbird import AvailableStations, RainbirdController
import voluptuous as vol
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice
from homeassistant.const import (
CONF_FRIENDLY_NAME,
CONF_SCAN_INTERVAL,
CONF_SWITCHES,
CONF_TRIGGER_TIME,
CONF_ZONE,
)
from homeassistant.components.switch import SwitchDevice
from homeassistant.const import ATTR_ENTITY_ID, CONF_FRIENDLY_NAME, CONF_TRIGGER_TIME
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__)
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(
{
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,
}
}
)
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
vol.Required(ATTR_DURATION): vol.All(vol.Coerce(float), vol.Range(min=0)),
}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""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 = []
for dev_id, switch in config.get(CONF_SWITCHES).items():
devices.append(RainBirdSwitch(controller, switch, dev_id))
for zone in range(1, available_stations.stations.count + 1):
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)
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):
"""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."""
self._rainbird = rb
self._devid = dev_id
self._zone = int(dev.get(CONF_ZONE))
self._name = dev.get(CONF_FRIENDLY_NAME, f"Sprinkler {self._zone}")
self._rainbird = controller
self._zone = zone
self._name = name
self._state = None
self._duration = dev.get(CONF_TRIGGER_TIME)
self._attributes = {"duration": self._duration, "zone": self._zone}
self._duration = time
self._attributes = {ATTR_DURATION: self._duration, "zone": self._zone}
@property
def device_state_attributes(self):
@ -70,27 +94,20 @@ class RainBirdSwitch(SwitchDevice):
def turn_on(self, **kwargs):
"""Turn the switch on."""
response = self._rainbird.startIrrigation(int(self._zone), int(self._duration))
if response and response["type"] == "AcknowledgeResponse":
if self._rainbird.irrigate_zone(
int(self._zone),
int(kwargs[ATTR_DURATION] if ATTR_DURATION in kwargs else self._duration),
):
self._state = True
def turn_off(self, **kwargs):
"""Turn the switch off."""
response = self._rainbird.stopIrrigation()
if response and response["type"] == "AcknowledgeResponse":
if self._rainbird.stop_irrigation():
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):
"""Update switch status."""
self._state = self.get_device_status()
self._state = self._rainbird.get_zone_state(self._zone)
@property
def is_on(self):

View File

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