Home Assistant Git Exporter
This commit is contained in:
58
config/custom_components/irrigation_unlimited/__init__.py
Normal file
58
config/custom_components/irrigation_unlimited/__init__.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
Custom integration to integrate irrigation_unlimited with Home Assistant.
|
||||
|
||||
For more details about this integration, please refer to
|
||||
https://github.com/rgc99/irrigation_unlimited
|
||||
"""
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.core import Config, HomeAssistant
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
|
||||
from .irrigation_unlimited import IUCoordinator
|
||||
from .entity import IUComponent
|
||||
from .service import register_component_services
|
||||
|
||||
from .schema import (
|
||||
IRRIGATION_SCHEMA,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
BINARY_SENSOR,
|
||||
DOMAIN,
|
||||
COORDINATOR,
|
||||
COMPONENT,
|
||||
STARTUP_MESSAGE,
|
||||
)
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__package__)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({DOMAIN: IRRIGATION_SCHEMA}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: Config):
|
||||
"""Set up this integration using YAML."""
|
||||
|
||||
_LOGGER.info(STARTUP_MESSAGE)
|
||||
|
||||
hass.data[DOMAIN] = {}
|
||||
coordinator = IUCoordinator(hass).load(config[DOMAIN])
|
||||
hass.data[DOMAIN][COORDINATOR] = coordinator
|
||||
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||
hass.data[DOMAIN][COMPONENT] = component
|
||||
|
||||
await component.async_add_entities([IUComponent(coordinator)])
|
||||
|
||||
await hass.async_create_task(
|
||||
async_load_platform(hass, BINARY_SENSOR, DOMAIN, {}, config)
|
||||
)
|
||||
|
||||
register_component_services(component, coordinator)
|
||||
|
||||
coordinator.listen()
|
||||
coordinator.clock.start()
|
||||
|
||||
return True
|
||||
354
config/custom_components/irrigation_unlimited/binary_sensor.py
Normal file
354
config/custom_components/irrigation_unlimited/binary_sensor.py
Normal file
@@ -0,0 +1,354 @@
|
||||
"""Binary sensor platform for irrigation_unlimited."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
EntityPlatform,
|
||||
current_platform,
|
||||
async_get_platforms,
|
||||
)
|
||||
from homeassistant.util import dt
|
||||
|
||||
from .irrigation_unlimited import IUCoordinator
|
||||
from .entity import IUEntity
|
||||
from .service import register_platform_services
|
||||
from .const import (
|
||||
ATTR_ENABLED,
|
||||
ATTR_SEQUENCE_STATUS,
|
||||
ATTR_STATUS,
|
||||
ATTR_INDEX,
|
||||
ATTR_CURRENT_SCHEDULE,
|
||||
ATTR_CURRENT_NAME,
|
||||
ATTR_CURRENT_ADJUSTMENT,
|
||||
ATTR_CURRENT_START,
|
||||
ATTR_CURRENT_DURATION,
|
||||
ATTR_NEXT_SCHEDULE,
|
||||
ATTR_NEXT_ZONE,
|
||||
ATTR_NEXT_NAME,
|
||||
ATTR_NEXT_ADJUSTMENT,
|
||||
ATTR_NEXT_START,
|
||||
ATTR_NEXT_DURATION,
|
||||
ATTR_TIME_REMAINING,
|
||||
ATTR_PERCENT_COMPLETE,
|
||||
ATTR_ZONE_COUNT,
|
||||
ATTR_CURRENT_ZONE,
|
||||
ATTR_TOTAL_TODAY,
|
||||
ATTR_SCHEDULE_COUNT,
|
||||
ATTR_ADJUSTMENT,
|
||||
ATTR_CONFIGURATION,
|
||||
ATTR_TIMELINE,
|
||||
ATTR_SUSPENDED,
|
||||
BINARY_SENSOR,
|
||||
DOMAIN,
|
||||
COORDINATOR,
|
||||
CONF_SCHEDULES,
|
||||
CONF_ZONE_ID,
|
||||
RES_MANUAL,
|
||||
RES_NOT_RUNNING,
|
||||
RES_NONE,
|
||||
ATTR_VOLUME,
|
||||
ATTR_FLOW_RATE,
|
||||
ATTR_SEQUENCE_COUNT,
|
||||
ATTR_ZONES,
|
||||
)
|
||||
|
||||
|
||||
def find_platform(hass: HomeAssistant, name: str) -> EntityPlatform:
|
||||
"""Find a platform in our domain"""
|
||||
platforms = async_get_platforms(hass, DOMAIN)
|
||||
for platform in platforms:
|
||||
if platform.domain == name:
|
||||
return platform
|
||||
return None
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None
|
||||
) -> None:
|
||||
"""Setup binary_sensor platform."""
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
coordinator: IUCoordinator = hass.data[DOMAIN][COORDINATOR]
|
||||
entities = []
|
||||
for controller in coordinator.controllers:
|
||||
entities.append(IUMasterEntity(coordinator, controller, None, None))
|
||||
for zone in controller.zones:
|
||||
entities.append(IUZoneEntity(coordinator, controller, zone, None))
|
||||
for sequence in controller.sequences:
|
||||
entities.append(IUSequenceEntity(coordinator, controller, None, sequence))
|
||||
async_add_entities(entities)
|
||||
|
||||
platform = current_platform.get()
|
||||
register_platform_services(platform)
|
||||
|
||||
return
|
||||
|
||||
|
||||
async def async_reload_platform(
|
||||
component: EntityComponent, coordinator: IUCoordinator
|
||||
) -> bool:
|
||||
"""Handle the reloading of this platform"""
|
||||
|
||||
def remove_entity(entities: "dict[Entity]", entity_id: str) -> bool:
|
||||
entity_id = f"{BINARY_SENSOR}.{DOMAIN}_{entity_id}"
|
||||
if entity_id in entities:
|
||||
entities.pop(entity_id)
|
||||
return True
|
||||
return False
|
||||
|
||||
platform: EntityPlatform = find_platform(component.hass, BINARY_SENSOR)
|
||||
if platform is None:
|
||||
return False
|
||||
|
||||
old_entities: dict[Entity] = platform.entities.copy()
|
||||
new_entities: list[Entity] = []
|
||||
|
||||
for controller in coordinator.controllers:
|
||||
if not remove_entity(old_entities, controller.unique_id):
|
||||
new_entities.append(IUMasterEntity(coordinator, controller, None, None))
|
||||
for zone in controller.zones:
|
||||
if not remove_entity(old_entities, zone.unique_id):
|
||||
new_entities.append(IUZoneEntity(coordinator, controller, zone, None))
|
||||
for sequence in controller.sequences:
|
||||
if not remove_entity(old_entities, sequence.unique_id):
|
||||
new_entities.append(
|
||||
IUSequenceEntity(coordinator, controller, None, sequence)
|
||||
)
|
||||
if len(new_entities) > 0:
|
||||
await platform.async_add_entities(new_entities)
|
||||
coordinator.initialise()
|
||||
for entity in old_entities:
|
||||
await platform.async_remove_entity(entity)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class IUMasterEntity(IUEntity):
|
||||
"""irrigation_unlimited controller binary_sensor class."""
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return self._controller.unique_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the friendly name of the binary_sensor."""
|
||||
return self._controller.name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary_sensor is on."""
|
||||
return self._controller.is_on
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Indicate that we need to poll data"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend."""
|
||||
return self._controller.icon
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return the state attributes of the device."""
|
||||
attr = {}
|
||||
attr[ATTR_INDEX] = self._controller.index
|
||||
attr[ATTR_ENABLED] = self._controller.enabled
|
||||
attr[ATTR_SUSPENDED] = self._controller.suspended
|
||||
attr[ATTR_STATUS] = self._controller.status
|
||||
attr[ATTR_ZONE_COUNT] = len(self._controller.zones)
|
||||
attr[ATTR_SEQUENCE_COUNT] = len(self._controller.sequences)
|
||||
attr[ATTR_ZONES] = ""
|
||||
attr[ATTR_SEQUENCE_STATUS] = self._controller.sequence_status()
|
||||
current = self._controller.runs.current_run
|
||||
if current is not None:
|
||||
attr[ATTR_CURRENT_ZONE] = current.zone.index + 1
|
||||
attr[ATTR_CURRENT_NAME] = current.zone.name
|
||||
attr[ATTR_CURRENT_START] = dt.as_local(current.start_time)
|
||||
attr[ATTR_CURRENT_DURATION] = str(current.duration)
|
||||
attr[ATTR_TIME_REMAINING] = str(current.time_remaining)
|
||||
attr[ATTR_PERCENT_COMPLETE] = current.percent_complete
|
||||
else:
|
||||
attr[ATTR_CURRENT_SCHEDULE] = "deprecated (use current_zone)"
|
||||
attr[ATTR_CURRENT_ZONE] = RES_NOT_RUNNING
|
||||
attr[ATTR_PERCENT_COMPLETE] = 0
|
||||
|
||||
next_run = self._controller.runs.next_run
|
||||
if next_run is not None:
|
||||
attr[ATTR_NEXT_ZONE] = next_run.zone.index + 1
|
||||
attr[ATTR_NEXT_NAME] = next_run.zone.name
|
||||
attr[ATTR_NEXT_START] = dt.as_local(next_run.start_time)
|
||||
attr[ATTR_NEXT_DURATION] = str(next_run.duration)
|
||||
else:
|
||||
attr[ATTR_NEXT_SCHEDULE] = "deprecated (use next_zone)"
|
||||
attr[ATTR_NEXT_ZONE] = RES_NONE
|
||||
attr[ATTR_VOLUME] = self._controller.volume.total
|
||||
attr[ATTR_FLOW_RATE] = self._controller.volume.flow_rate
|
||||
attr |= self._controller.user
|
||||
return attr
|
||||
|
||||
|
||||
class IUZoneEntity(IUEntity):
|
||||
"""irrigation_unlimited zone binary_sensor class."""
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return self._zone.unique_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the friendly name of the binary_sensor."""
|
||||
return self._zone.name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary_sensor is on."""
|
||||
return self._zone.is_on
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Indicate that we need to poll data"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend."""
|
||||
return self._zone.icon
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return the state attributes of the device."""
|
||||
# pylint: disable=too-many-branches
|
||||
|
||||
attr = {}
|
||||
attr[CONF_ZONE_ID] = self._zone.zone_id
|
||||
attr[ATTR_INDEX] = self._zone.index
|
||||
attr[ATTR_ENABLED] = self._zone.enabled
|
||||
attr[ATTR_SUSPENDED] = self._zone.suspended
|
||||
attr[ATTR_STATUS] = self._zone.status
|
||||
attr[ATTR_SCHEDULE_COUNT] = len(self._zone.schedules)
|
||||
attr[CONF_SCHEDULES] = ""
|
||||
attr[ATTR_ADJUSTMENT] = str(self._zone.adjustment)
|
||||
current = self._zone.runs.current_run
|
||||
if current is not None:
|
||||
attr[ATTR_CURRENT_ADJUSTMENT] = current.adjustment
|
||||
if current.schedule is not None:
|
||||
attr[ATTR_CURRENT_SCHEDULE] = current.schedule.index + 1
|
||||
attr[ATTR_CURRENT_NAME] = current.schedule.name
|
||||
else:
|
||||
attr[ATTR_CURRENT_SCHEDULE] = 0
|
||||
attr[ATTR_CURRENT_NAME] = RES_MANUAL
|
||||
attr[ATTR_CURRENT_START] = dt.as_local(current.start_time)
|
||||
attr[ATTR_CURRENT_DURATION] = str(current.duration)
|
||||
attr[ATTR_TIME_REMAINING] = str(current.time_remaining)
|
||||
attr[ATTR_PERCENT_COMPLETE] = current.percent_complete
|
||||
else:
|
||||
attr[ATTR_CURRENT_SCHEDULE] = None
|
||||
attr[ATTR_PERCENT_COMPLETE] = 0
|
||||
|
||||
next_run = self._zone.runs.next_run
|
||||
if next_run is not None:
|
||||
attr[ATTR_NEXT_ADJUSTMENT] = next_run.adjustment
|
||||
if next_run.schedule is not None:
|
||||
attr[ATTR_NEXT_SCHEDULE] = next_run.schedule.index + 1
|
||||
attr[ATTR_NEXT_NAME] = next_run.schedule.name
|
||||
else:
|
||||
attr[ATTR_NEXT_SCHEDULE] = 0
|
||||
attr[ATTR_NEXT_NAME] = RES_MANUAL
|
||||
attr[ATTR_NEXT_START] = dt.as_local(next_run.start_time)
|
||||
attr[ATTR_NEXT_DURATION] = str(next_run.duration)
|
||||
else:
|
||||
attr[ATTR_NEXT_SCHEDULE] = None
|
||||
attr[ATTR_TOTAL_TODAY] = round(
|
||||
self._zone.today_total.total_seconds() / 60,
|
||||
1,
|
||||
)
|
||||
if self._zone.show_config:
|
||||
attr[ATTR_CONFIGURATION] = self._zone.configuration
|
||||
if self._zone.show_timeline:
|
||||
attr[ATTR_TIMELINE] = self._zone.timeline()
|
||||
attr[ATTR_VOLUME] = self._zone.volume.total
|
||||
attr[ATTR_FLOW_RATE] = self._zone.volume.flow_rate
|
||||
attr |= self._zone.user
|
||||
return attr
|
||||
|
||||
|
||||
class IUSequenceEntity(IUEntity):
|
||||
"""irrigation_unlimited sequence binary_sensor class."""
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return self._sequence.unique_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the friendly name of the binary_sensor."""
|
||||
return self._sequence.name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary_sensor is on."""
|
||||
return self._sequence.is_on
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Indicate that we need to poll data"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend."""
|
||||
return self._sequence.icon
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return the state attributes of the device."""
|
||||
attr = {}
|
||||
attr[ATTR_INDEX] = self._sequence.index
|
||||
attr[ATTR_ENABLED] = self._sequence.enabled
|
||||
attr[ATTR_SUSPENDED] = (
|
||||
dt.as_local(self._sequence.suspended) if self._sequence.suspended else None
|
||||
)
|
||||
attr[ATTR_STATUS] = self._sequence.status
|
||||
attr[ATTR_ZONE_COUNT] = len(self._sequence.zones)
|
||||
attr[ATTR_SCHEDULE_COUNT] = len(self._sequence.schedules)
|
||||
attr[ATTR_ADJUSTMENT] = str(self._sequence.adjustment)
|
||||
attr[ATTR_VOLUME] = self._sequence.volume
|
||||
if (current := self._sequence.runs.current_run) is not None:
|
||||
if current.active_zone is not None:
|
||||
attr[ATTR_CURRENT_ZONE] = current.active_zone.sequence_zone.id1
|
||||
else:
|
||||
attr[ATTR_CURRENT_ZONE] = None
|
||||
attr[ATTR_CURRENT_START] = dt.as_local(current.start_time)
|
||||
attr[ATTR_CURRENT_DURATION] = str(current.total_time)
|
||||
attr[ATTR_TIME_REMAINING] = str(current.time_remaining)
|
||||
attr[ATTR_PERCENT_COMPLETE] = current.percent_complete
|
||||
if current.schedule is not None:
|
||||
attr[ATTR_CURRENT_SCHEDULE] = current.schedule.id1
|
||||
attr[ATTR_CURRENT_NAME] = current.schedule.name
|
||||
else:
|
||||
attr[ATTR_CURRENT_SCHEDULE] = 0
|
||||
attr[ATTR_CURRENT_NAME] = RES_MANUAL
|
||||
else:
|
||||
attr[ATTR_CURRENT_ZONE] = None
|
||||
attr[ATTR_CURRENT_SCHEDULE] = None
|
||||
attr[ATTR_PERCENT_COMPLETE] = 0
|
||||
if (next_run := self._sequence.runs.next_run) is not None:
|
||||
attr[ATTR_NEXT_START] = dt.as_local(next_run.start_time)
|
||||
attr[ATTR_NEXT_DURATION] = str(next_run.total_time)
|
||||
if next_run.schedule is not None:
|
||||
attr[ATTR_NEXT_SCHEDULE] = next_run.schedule.id1
|
||||
attr[ATTR_NEXT_NAME] = next_run.schedule.name
|
||||
else:
|
||||
attr[ATTR_NEXT_SCHEDULE] = 0
|
||||
attr[ATTR_NEXT_NAME] = RES_MANUAL
|
||||
else:
|
||||
attr[ATTR_NEXT_SCHEDULE] = None
|
||||
attr[ATTR_ZONES] = self._sequence.ha_zone_attr()
|
||||
return attr
|
||||
249
config/custom_components/irrigation_unlimited/const.py
Normal file
249
config/custom_components/irrigation_unlimited/const.py
Normal file
@@ -0,0 +1,249 @@
|
||||
"""Constants for irrigation_unlimited."""
|
||||
|
||||
# Base component constants
|
||||
NAME = "Irrigation Unlimited"
|
||||
DOMAIN = "irrigation_unlimited"
|
||||
DOMAIN_DATA = f"{DOMAIN}_data"
|
||||
COORDINATOR = "coordinator"
|
||||
COMPONENT = "component"
|
||||
VERSION = "2024.5.0"
|
||||
ATTRIBUTION = "Data provided by http://jsonplaceholder.typicode.com/"
|
||||
ISSUE_URL = "https://github.com/rgc99/irrigation_unlimited/issues"
|
||||
|
||||
# Icons
|
||||
ICON = "mdi:zodiac-aquarius"
|
||||
ICON_ZONE_ON = "mdi:valve-open"
|
||||
ICON_ZONE_OFF = "mdi:valve-closed"
|
||||
ICON_DISABLED = "mdi:circle-off-outline"
|
||||
ICON_SUSPENDED = "mdi:timer-outline"
|
||||
ICON_BLOCKED = "mdi:alert-octagon-outline"
|
||||
ICON_CONTROLLER_ON = "mdi:water"
|
||||
ICON_CONTROLLER_OFF = "mdi:water-off"
|
||||
ICON_CONTROLLER_PAUSED = "mdi:pause-circle-outline"
|
||||
ICON_CONTROLLER_DELAY = "mdi:timer-sand"
|
||||
ICON_SEQUENCE_ON = "mdi:play-circle-outline"
|
||||
ICON_SEQUENCE_OFF = "mdi:stop-circle-outline"
|
||||
ICON_SEQUENCE_PAUSED = "mdi:pause-circle-outline"
|
||||
ICON_SEQUENCE_ZONE_ON = "mdi:play-circle-outline"
|
||||
ICON_SEQUENCE_ZONE_OFF = "mdi:stop-circle-outline"
|
||||
ICON_SEQUENCE_DELAY = "mdi:timer-sand"
|
||||
|
||||
# Platforms
|
||||
BINARY_SENSOR = "binary_sensor"
|
||||
PLATFORMS = [BINARY_SENSOR]
|
||||
|
||||
# Configuration and options
|
||||
CONF_ENABLED = "enabled"
|
||||
CONF_CONTROLLER = "controller"
|
||||
CONF_CONTROLLERS = "controllers"
|
||||
CONF_ZONE = "zone"
|
||||
CONF_ZONES = "zones"
|
||||
CONF_SCHEDULE = "schedule"
|
||||
CONF_SCHEDULES = "schedules"
|
||||
CONF_SUN = "sun"
|
||||
CONF_TIME = "time"
|
||||
CONF_DURATION = "duration"
|
||||
CONF_PREAMBLE = "preamble"
|
||||
CONF_POSTAMBLE = "postamble"
|
||||
CONF_TESTING = "testing"
|
||||
CONF_SPEED = "speed"
|
||||
CONF_START = "start"
|
||||
CONF_END = "end"
|
||||
CONF_TIMES = "times"
|
||||
CONF_GRANULARITY = "granularity"
|
||||
CONF_PERCENTAGE = "percentage"
|
||||
CONF_ACTUAL = "actual"
|
||||
CONF_INCREASE = "increase"
|
||||
CONF_DECREASE = "decrease"
|
||||
CONF_RESET = "reset"
|
||||
CONF_MINIMUM = "minimum"
|
||||
CONF_MAXIMUM = "maximum"
|
||||
CONF_MONTH = "month"
|
||||
CONF_DAY = "day"
|
||||
CONF_ODD = "odd"
|
||||
CONF_EVEN = "even"
|
||||
CONF_SHOW = "show"
|
||||
CONF_CONFIG = "config"
|
||||
CONF_TIMELINE = "timeline"
|
||||
CONF_CONTROLLER_ID = "controller_id"
|
||||
CONF_ZONE_ID = "zone_id"
|
||||
CONF_SEQUENCE_ID = "sequence_id"
|
||||
CONF_SEQUENCE = "sequence"
|
||||
CONF_SEQUENCES = "sequences"
|
||||
CONF_SEQUENCE_ZONE = "sequence_zone"
|
||||
CONF_SEQUENCE_ZONES = "sequence_zones"
|
||||
CONF_ALL_ZONES_CONFIG = "all_zones_config"
|
||||
CONF_REFRESH_INTERVAL = "refresh_interval"
|
||||
CONF_INDEX = "index"
|
||||
CONF_RESULTS = "results"
|
||||
CONF_OUTPUT_EVENTS = "output_events"
|
||||
CONF_SHOW_LOG = "show_log"
|
||||
CONF_AUTOPLAY = "autoplay"
|
||||
CONF_FUTURE_SPAN = "future_span"
|
||||
CONF_HISTORY = "history"
|
||||
CONF_HISTORY_SPAN = "history_span"
|
||||
CONF_SPAN = "span"
|
||||
CONF_HISTORY_REFRESH = "history_refresh"
|
||||
CONF_ANCHOR = "anchor"
|
||||
CONF_FINISH = "finish"
|
||||
CONF_LOGGING = "logging"
|
||||
CONF_VERSION = "version"
|
||||
CONF_RUN = "run"
|
||||
CONF_SYNC_SWITCHES = "sync_switches"
|
||||
CONF_RENAME_ENTITIES = "rename_entities"
|
||||
CONF_ENTITY_BASE = "entity_base"
|
||||
CONF_CLOCK = "clock"
|
||||
CONF_MODE = "mode"
|
||||
CONF_FIXED = "fixed"
|
||||
CONF_SEER = "seer"
|
||||
CONF_MAX_LOG_ENTRIES = "max_log_entries"
|
||||
DEFAULT_MAX_LOG_ENTRIES = 50
|
||||
CONF_ALLOW_MANUAL = "allow_manual"
|
||||
CONF_CRON = "cron"
|
||||
CONF_EVERY_N_DAYS = "every_n_days"
|
||||
CONF_START_N_DAYS = "start_n_days"
|
||||
CONF_CHECK_BACK = "check_back"
|
||||
CONF_STATES = "states"
|
||||
CONF_RETRIES = "retries"
|
||||
CONF_RESYNC = "resync"
|
||||
CONF_EXPECTED = "expected"
|
||||
CONF_FOUND = "found"
|
||||
CONF_STATE_ON = "state_on"
|
||||
CONF_STATE_OFF = "state_off"
|
||||
CONF_SCHEDULE_ID = "schedule_id"
|
||||
CONF_FROM = "from"
|
||||
CONF_VOLUME = "volume"
|
||||
CONF_VOLUME_PRECISION = "volume_precision"
|
||||
CONF_VOLUME_SCALE = "volume_scale"
|
||||
CONF_FLOW_RATE_PRECISION = "flow_rate_precision"
|
||||
CONF_FLOW_RATE_SCALE = "flow_rate_scale"
|
||||
CONF_QUEUE = "queue"
|
||||
CONF_QUEUE_MANUAL = "queue_manual"
|
||||
CONF_USER = "user"
|
||||
CONF_TOGGLE = "toggle"
|
||||
CONF_EXTENDED_CONFIG = "extended_config"
|
||||
|
||||
# Defaults
|
||||
DEFAULT_NAME = DOMAIN
|
||||
DEFAULT_GRANULARITY = 60
|
||||
DEFAULT_TEST_SPEED = 1.0
|
||||
DEFAULT_REFRESH_INTERVAL = 30
|
||||
|
||||
# Services
|
||||
SERVICE_ENABLE = "enable"
|
||||
SERVICE_DISABLE = "disable"
|
||||
SERVICE_TOGGLE = "toggle"
|
||||
SERVICE_CANCEL = "cancel"
|
||||
SERVICE_TIME_ADJUST = "adjust_time"
|
||||
SERVICE_MANUAL_RUN = "manual_run"
|
||||
SERVICE_LOAD_SCHEDULE = "load_schedule"
|
||||
SERVICE_SUSPEND = "suspend"
|
||||
SERVICE_SKIP = "skip"
|
||||
SERVICE_PAUSE = "pause"
|
||||
SERVICE_RESUME = "resume"
|
||||
|
||||
# Events
|
||||
EVENT_START = "start"
|
||||
EVENT_FINISH = "finish"
|
||||
EVENT_INCOMPLETE = "incomplete"
|
||||
EVENT_SYNC_ERROR = "sync_error"
|
||||
EVENT_SWITCH_ERROR = "switch_error"
|
||||
|
||||
# Status
|
||||
STATUS_DISABLED = "disabled"
|
||||
STATUS_SUSPENDED = "suspended"
|
||||
STATUS_BLOCKED = "blocked"
|
||||
STATUS_INITIALISING = "initialising"
|
||||
STATUS_PAUSED = "paused"
|
||||
STATUS_DELAY = "delay"
|
||||
|
||||
# Timeline labels
|
||||
TIMELINE_STATUS = "status"
|
||||
TIMELINE_START = "start"
|
||||
TIMELINE_END = "end"
|
||||
TIMELINE_SCHEDULE_NAME = "schedule_name"
|
||||
TIMELINE_ADJUSTMENT = "adjustment"
|
||||
|
||||
# Attributes
|
||||
ATTR_ENABLED = "enabled"
|
||||
ATTR_STATUS = "status"
|
||||
ATTR_INDEX = "index"
|
||||
ATTR_NAME = "name"
|
||||
ATTR_CURRENT_SCHEDULE = "current_schedule"
|
||||
ATTR_CURRENT_NAME = "current_name"
|
||||
ATTR_CURRENT_ADJUSTMENT = "current_adjustment"
|
||||
ATTR_CURRENT_START = "current_start"
|
||||
ATTR_CURRENT_DURATION = "current_duration"
|
||||
ATTR_BASE_DURATION = "base_duration"
|
||||
ATTR_DEFAULT_DURATION = "default_duration"
|
||||
ATTR_DEFAULT_DELAY = "default_delay"
|
||||
ATTR_ADJUSTED_DURATION = "adjusted_duration"
|
||||
ATTR_FINAL_DURATION = "final_duration"
|
||||
ATTR_TOTAL_DELAY = "total_delay"
|
||||
ATTR_TOTAL_DURATION = "total_duration"
|
||||
ATTR_DURATION_FACTOR = "duration_factor"
|
||||
ATTR_NEXT_SCHEDULE = "next_schedule"
|
||||
ATTR_NEXT_ZONE = "next_zone"
|
||||
ATTR_NEXT_NAME = "next_name"
|
||||
ATTR_NEXT_ADJUSTMENT = "next_adjustment"
|
||||
ATTR_NEXT_START = "next_start"
|
||||
ATTR_NEXT_DURATION = "next_duration"
|
||||
ATTR_TIME_REMAINING = "time_remaining"
|
||||
ATTR_PERCENT_COMPLETE = "percent_complete"
|
||||
ATTR_ZONE_COUNT = "zone_count"
|
||||
ATTR_CURRENT_ZONE = "current_zone"
|
||||
ATTR_TOTAL_TODAY = "today_total"
|
||||
ATTR_SCHEDULE_COUNT = "schedule_count"
|
||||
ATTR_ADJUSTMENT = "adjustment"
|
||||
ATTR_CONFIGURATION = "configuration"
|
||||
ATTR_TIMELINE = "timeline"
|
||||
ATTR_SCHEDULE = "schedule"
|
||||
ATTR_START = "start"
|
||||
ATTR_DURATION = "duration"
|
||||
ATTR_ZONES = "zones"
|
||||
ATTR_SEQUENCE_STATUS = "sequence_status"
|
||||
ATTR_ZONE_IDS = "zone_ids"
|
||||
ATTR_CONTROLLER_COUNT = "controller_count"
|
||||
ATTR_NEXT_TICK = "next_tick"
|
||||
ATTR_TICK_LOG = "tick_log"
|
||||
ATTR_SUSPENDED = "suspended"
|
||||
ATTR_VOLUME = "volume"
|
||||
ATTR_FLOW_RATE = "flow_rate"
|
||||
ATTR_SWITCH_ENTITIES = "switch_entity_id"
|
||||
ATTR_SEQUENCE_COUNT = "sequence_count"
|
||||
|
||||
# Resources
|
||||
RES_MANUAL = "Manual"
|
||||
RES_NOT_RUNNING = "not running"
|
||||
RES_NONE = "none"
|
||||
RES_CONTROLLER = "Controller"
|
||||
RES_ZONE = "Zone"
|
||||
RES_MASTER = "Master"
|
||||
RES_TIMELINE_RUNNING = "running"
|
||||
RES_TIMELINE_SCHEDULED = "scheduled"
|
||||
RES_TIMELINE_NEXT = "next"
|
||||
RES_TIMELINE_HISTORY = "history"
|
||||
|
||||
MONTHS = [
|
||||
"jan",
|
||||
"feb",
|
||||
"mar",
|
||||
"apr",
|
||||
"may",
|
||||
"jun",
|
||||
"jul",
|
||||
"aug",
|
||||
"sep",
|
||||
"oct",
|
||||
"nov",
|
||||
"dec",
|
||||
]
|
||||
|
||||
STARTUP_MESSAGE = f"""
|
||||
-------------------------------------------------------------------
|
||||
{NAME}
|
||||
Version: {VERSION}
|
||||
If you have any issues with this you need to open an issue here:
|
||||
{ISSUE_URL}
|
||||
-------------------------------------------------------------------
|
||||
"""
|
||||
386
config/custom_components/irrigation_unlimited/entity.py
Normal file
386
config/custom_components/irrigation_unlimited/entity.py
Normal file
@@ -0,0 +1,386 @@
|
||||
"""HA entity classes"""
|
||||
|
||||
import json
|
||||
from collections.abc import Iterator
|
||||
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||
from homeassistant.core import ServiceCall
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.util import dt
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_STATE,
|
||||
CONF_UNTIL,
|
||||
STATE_OK,
|
||||
STATE_ON,
|
||||
)
|
||||
|
||||
from .irrigation_unlimited import (
|
||||
IUCoordinator,
|
||||
IUController,
|
||||
IUZone,
|
||||
IUSequence,
|
||||
IUSequenceZone,
|
||||
IUAdjustment,
|
||||
IUBase,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
ATTR_ADJUSTMENT,
|
||||
ATTR_CONFIGURATION,
|
||||
ATTR_CONTROLLER_COUNT,
|
||||
ATTR_ENABLED,
|
||||
ATTR_NEXT_TICK,
|
||||
ATTR_TICK_LOG,
|
||||
ATTR_SUSPENDED,
|
||||
CONF_CONTROLLER,
|
||||
CONF_CONTROLLERS,
|
||||
CONF_ENABLED,
|
||||
CONF_INDEX,
|
||||
CONF_RESET,
|
||||
CONF_SEQUENCE,
|
||||
CONF_SEQUENCE_ID,
|
||||
CONF_SEQUENCE_ZONE,
|
||||
CONF_SEQUENCE_ZONES,
|
||||
CONF_SEQUENCES,
|
||||
CONF_ZONE,
|
||||
CONF_ZONES,
|
||||
COORDINATOR,
|
||||
ICON,
|
||||
SERVICE_ENABLE,
|
||||
SERVICE_DISABLE,
|
||||
SERVICE_TIME_ADJUST,
|
||||
SERVICE_SUSPEND,
|
||||
STATUS_INITIALISING,
|
||||
)
|
||||
|
||||
|
||||
class IURestore:
|
||||
"""Restore class"""
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
||||
def __init__(self, data: dict, coordinator: IUCoordinator):
|
||||
self._coordinator = coordinator
|
||||
self._is_on = []
|
||||
|
||||
if data is not None and isinstance(data, dict):
|
||||
for c_data in data.get(CONF_CONTROLLERS, []):
|
||||
self._restore_controller(c_data)
|
||||
|
||||
@property
|
||||
def is_on(self) -> list:
|
||||
"""Return the list of objects left in on state"""
|
||||
return self._is_on
|
||||
|
||||
def _add_to_on_list(
|
||||
self,
|
||||
controller: IUController,
|
||||
zone: IUZone = None,
|
||||
sequence: IUSequence = None,
|
||||
sequence_zone: IUSequenceZone = None,
|
||||
) -> None:
|
||||
# pylint: disable=too-many-arguments
|
||||
if sequence_zone is None:
|
||||
if sequence is not None:
|
||||
for item in self._is_on:
|
||||
if item[CONF_SEQUENCE] == sequence:
|
||||
return
|
||||
elif zone is not None:
|
||||
for item in self._is_on:
|
||||
if item[CONF_ZONE] == zone:
|
||||
return
|
||||
elif controller is not None:
|
||||
for item in self._is_on:
|
||||
if item[CONF_CONTROLLER] == controller:
|
||||
return
|
||||
self._is_on.append(
|
||||
{
|
||||
CONF_CONTROLLER: controller,
|
||||
CONF_ZONE: zone,
|
||||
CONF_SEQUENCE: sequence,
|
||||
CONF_SEQUENCE_ZONE: sequence_zone,
|
||||
}
|
||||
)
|
||||
return
|
||||
|
||||
def _check_is_on(
|
||||
self,
|
||||
data: dict,
|
||||
controller: IUController,
|
||||
zone: IUZone,
|
||||
sequence: IUSequence,
|
||||
sequence_zone: IUSequenceZone,
|
||||
) -> None:
|
||||
# pylint: disable=too-many-arguments
|
||||
if not CONF_STATE in data:
|
||||
return
|
||||
if data.get(CONF_STATE) == STATE_ON:
|
||||
if sequence_zone is not None:
|
||||
items = data.get(CONF_ZONES)
|
||||
if isinstance(items, str):
|
||||
items = items.split(",") # Old style 1's based CSV
|
||||
for item in items:
|
||||
try:
|
||||
item = int(item)
|
||||
zne = controller.get_zone(item - sequence_zone.ZONE_OFFSET)
|
||||
if zne is not None:
|
||||
self._add_to_on_list(
|
||||
controller, zne, sequence, sequence_zone
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
self._add_to_on_list(controller, zone, sequence, sequence_zone)
|
||||
|
||||
def _restore_enabled(
|
||||
self,
|
||||
data: dict,
|
||||
controller: IUController,
|
||||
zone: IUZone,
|
||||
sequence: IUSequence,
|
||||
sequence_zone: IUSequenceZone,
|
||||
) -> None:
|
||||
# pylint: disable=too-many-arguments
|
||||
if not CONF_ENABLED in data:
|
||||
return
|
||||
svc = SERVICE_ENABLE if data.get(CONF_ENABLED) else SERVICE_DISABLE
|
||||
svd = {}
|
||||
if sequence is not None:
|
||||
svd[CONF_SEQUENCE_ID] = sequence.index + 1
|
||||
if sequence_zone is not None:
|
||||
svd[CONF_ZONES] = [sequence_zone.index + 1]
|
||||
self._coordinator.service_call(svc, controller, zone, None, svd)
|
||||
|
||||
def _restore_suspend(
|
||||
self,
|
||||
data: dict,
|
||||
controller: IUController,
|
||||
zone: IUZone,
|
||||
sequence: IUSequence,
|
||||
sequence_zone: IUSequenceZone,
|
||||
) -> None:
|
||||
# pylint: disable=too-many-arguments
|
||||
if not ATTR_SUSPENDED in data:
|
||||
return
|
||||
svd = {}
|
||||
if data.get(ATTR_SUSPENDED) is not None:
|
||||
svd[CONF_UNTIL] = dt.parse_datetime(data.get(ATTR_SUSPENDED))
|
||||
else:
|
||||
svd[CONF_RESET] = None
|
||||
if sequence is not None:
|
||||
svd[CONF_SEQUENCE_ID] = sequence.index + 1
|
||||
if sequence_zone is not None:
|
||||
svd[CONF_ZONES] = [sequence_zone.index + 1]
|
||||
self._coordinator.service_call(SERVICE_SUSPEND, controller, zone, None, svd)
|
||||
|
||||
def _restore_adjustment(
|
||||
self,
|
||||
data: dict,
|
||||
controller: IUController,
|
||||
zone: IUZone,
|
||||
sequence: IUSequence,
|
||||
sequence_zone: IUSequenceZone,
|
||||
) -> None:
|
||||
# pylint: disable=too-many-arguments
|
||||
if not ATTR_ADJUSTMENT in data:
|
||||
return
|
||||
if (svd := IUAdjustment(data.get(ATTR_ADJUSTMENT)).to_dict()) == {}:
|
||||
svd[CONF_RESET] = None
|
||||
if sequence is not None:
|
||||
svd[CONF_SEQUENCE_ID] = sequence.index + 1
|
||||
if sequence_zone is not None:
|
||||
svd[CONF_ZONES] = [sequence_zone.index + 1]
|
||||
self._coordinator.service_call(SERVICE_TIME_ADJUST, controller, zone, None, svd)
|
||||
|
||||
def _restore_sequence_zone(
|
||||
self, data: dict, controller: IUController, sequence: IUSequence
|
||||
) -> None:
|
||||
if (sequence_zone := sequence.get_zone(data.get(CONF_INDEX))) is None:
|
||||
return
|
||||
self._restore_enabled(data, controller, None, sequence, sequence_zone)
|
||||
self._restore_suspend(data, controller, None, sequence, sequence_zone)
|
||||
self._restore_adjustment(data, controller, None, sequence, sequence_zone)
|
||||
self._check_is_on(data, controller, None, sequence, sequence_zone)
|
||||
|
||||
def _restore_sequence(self, data: dict, controller: IUController) -> None:
|
||||
if (sequence := controller.get_sequence(data.get(CONF_INDEX))) is None:
|
||||
return
|
||||
self._restore_enabled(data, controller, None, sequence, None)
|
||||
self._restore_suspend(data, controller, None, sequence, None)
|
||||
self._restore_adjustment(data, controller, None, sequence, None)
|
||||
for sz_data in data.get(CONF_SEQUENCE_ZONES, []):
|
||||
self._restore_sequence_zone(sz_data, controller, sequence)
|
||||
self._check_is_on(data, controller, None, sequence, None)
|
||||
|
||||
def _restore_zone(self, data: dict, controller: IUController) -> None:
|
||||
if (zone := controller.get_zone(data.get(CONF_INDEX))) is None:
|
||||
return
|
||||
self._restore_enabled(data, controller, zone, None, None)
|
||||
self._restore_suspend(data, controller, zone, None, None)
|
||||
self._restore_adjustment(data, controller, zone, None, None)
|
||||
self._check_is_on(data, controller, zone, None, None)
|
||||
|
||||
def _restore_controller(self, data: dict) -> None:
|
||||
if (controller := self._coordinator.get(data.get(CONF_INDEX))) is None:
|
||||
return
|
||||
self._restore_enabled(data, controller, None, None, None)
|
||||
self._restore_suspend(data, controller, None, None, None)
|
||||
for sq_data in data.get(CONF_SEQUENCES, []):
|
||||
self._restore_sequence(sq_data, controller)
|
||||
for z_data in data.get(CONF_ZONES, []):
|
||||
self._restore_zone(z_data, controller)
|
||||
self._check_is_on(data, controller, None, None, None)
|
||||
|
||||
def report_is_on(self) -> Iterator[str]:
|
||||
"""Generate a list of incomplete cycles"""
|
||||
for item in self._is_on:
|
||||
yield ",".join(
|
||||
IUBase.idl(
|
||||
[
|
||||
item[CONF_CONTROLLER],
|
||||
item[CONF_ZONE],
|
||||
item[CONF_SEQUENCE],
|
||||
item[CONF_SEQUENCE_ZONE],
|
||||
],
|
||||
"-",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class IUEntity(BinarySensorEntity, RestoreEntity):
|
||||
"""Base class for entities"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: IUCoordinator,
|
||||
controller: IUController,
|
||||
zone: IUZone,
|
||||
sequence: IUSequence,
|
||||
):
|
||||
"""Base entity class"""
|
||||
self._coordinator = coordinator
|
||||
self._controller = controller
|
||||
self._zone = zone # This will be None if it belongs to a Master/Controller
|
||||
self._sequence = sequence
|
||||
if self._sequence is not None:
|
||||
self.entity_id = self._sequence.entity_id
|
||||
elif self._zone is not None:
|
||||
self.entity_id = self._zone.entity_id
|
||||
else:
|
||||
self.entity_id = self._controller.entity_id
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
self._coordinator.register_entity(
|
||||
self._controller, self._zone, self._sequence, self
|
||||
)
|
||||
|
||||
# This code should be removed in future update. Moved to coordinator JSON configuration.
|
||||
if not self._coordinator.restored_from_configuration:
|
||||
state = await self.async_get_last_state()
|
||||
if state is None:
|
||||
return
|
||||
service = (
|
||||
SERVICE_ENABLE
|
||||
if state.attributes.get(ATTR_ENABLED, True)
|
||||
else SERVICE_DISABLE
|
||||
)
|
||||
self._coordinator.service_call(
|
||||
service, self._controller, self._zone, None, {}
|
||||
)
|
||||
return
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
self._coordinator.deregister_entity(
|
||||
self._controller, self._zone, self._sequence, self
|
||||
)
|
||||
return
|
||||
|
||||
def dispatch(self, service: str, call: ServiceCall) -> None:
|
||||
"""Dispatcher for service calls"""
|
||||
self._coordinator.service_call(
|
||||
service, self._controller, self._zone, self._sequence, call.data
|
||||
)
|
||||
|
||||
|
||||
class IUComponent(RestoreEntity):
|
||||
"""Representation of IrrigationUnlimitedCoordinator"""
|
||||
|
||||
def __init__(self, coordinator: IUCoordinator):
|
||||
self._coordinator = coordinator
|
||||
self.entity_id = self._coordinator.entity_id
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
self._coordinator.register_entity(None, None, None, self)
|
||||
state = await self.async_get_last_state()
|
||||
if state is None or ATTR_CONFIGURATION not in state.attributes:
|
||||
return
|
||||
json_data = state.attributes.get(ATTR_CONFIGURATION, {})
|
||||
try:
|
||||
data = json.loads(json_data)
|
||||
for item in IURestore(data, self._coordinator).is_on:
|
||||
controller: IUController = item[CONF_CONTROLLER]
|
||||
zone: IUZone = item[CONF_ZONE]
|
||||
sequence: IUSequence = item[CONF_SEQUENCE]
|
||||
sequence_zone: IUSequenceZone = item[CONF_SEQUENCE_ZONE]
|
||||
self._coordinator.logger.log_incomplete_cycle(
|
||||
controller,
|
||||
zone,
|
||||
sequence,
|
||||
sequence_zone,
|
||||
)
|
||||
self._coordinator.restored_from_configuration = True
|
||||
# pylint: disable=broad-except
|
||||
except Exception as exc:
|
||||
self._coordinator.logger.log_invalid_restore_data(repr(exc), json_data)
|
||||
return
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
self._coordinator.deregister_entity(None, None, None, self)
|
||||
return
|
||||
|
||||
def dispatch(self, service: str, call: ServiceCall) -> None:
|
||||
"""Service call dispatcher"""
|
||||
self._coordinator.service_call(service, None, None, None, call.data)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""If entity should be polled"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return COORDINATOR
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the integration."""
|
||||
return "Irrigation Unlimited Coordinator"
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the entity."""
|
||||
if not self._coordinator.initialised:
|
||||
return STATUS_INITIALISING
|
||||
return STATE_OK
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to be used for this entity"""
|
||||
return ICON
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attr = {}
|
||||
attr[ATTR_CONTROLLER_COUNT] = len(self._coordinator.controllers)
|
||||
attr[ATTR_CONFIGURATION] = self._coordinator.configuration
|
||||
if self._coordinator.clock.show_log:
|
||||
next_tick = self._coordinator.clock.next_tick
|
||||
attr[ATTR_NEXT_TICK] = (
|
||||
dt.as_local(next_tick) if next_tick is not None else None
|
||||
)
|
||||
attr[ATTR_TICK_LOG] = list(
|
||||
dt.as_local(tick) for tick in self._coordinator.clock.tick_log
|
||||
)
|
||||
return attr
|
||||
290
config/custom_components/irrigation_unlimited/history.py
Normal file
290
config/custom_components/irrigation_unlimited/history.py
Normal file
@@ -0,0 +1,290 @@
|
||||
"""History access and caching. This module runs asynchronously collecting
|
||||
and caching history data"""
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Callable, OrderedDict, Any
|
||||
from homeassistant.core import HomeAssistant, State, CALLBACK_TYPE
|
||||
from homeassistant.util import dt
|
||||
from homeassistant.components.recorder.const import DATA_INSTANCE as RECORDER_INSTANCE
|
||||
from homeassistant.components.recorder import get_instance
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_point_in_utc_time,
|
||||
)
|
||||
from homeassistant.components.recorder import history
|
||||
from homeassistant.const import STATE_ON
|
||||
|
||||
from .const import (
|
||||
ATTR_CURRENT_ADJUSTMENT,
|
||||
ATTR_CURRENT_NAME,
|
||||
CONF_ENABLED,
|
||||
CONF_HISTORY,
|
||||
CONF_HISTORY_REFRESH,
|
||||
CONF_HISTORY_SPAN,
|
||||
CONF_REFRESH_INTERVAL,
|
||||
CONF_SPAN,
|
||||
TIMELINE_ADJUSTMENT,
|
||||
TIMELINE_SCHEDULE_NAME,
|
||||
TIMELINE_START,
|
||||
TIMELINE_END,
|
||||
DOMAIN,
|
||||
BINARY_SENSOR,
|
||||
)
|
||||
|
||||
TIMELINE = "timeline"
|
||||
TODAY_ON = "today_on"
|
||||
|
||||
|
||||
def midnight(utc: datetime) -> datetime:
|
||||
"""Accept a UTC time and return midnight for that day"""
|
||||
return dt.as_utc(
|
||||
dt.as_local(utc).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
)
|
||||
|
||||
|
||||
def round_seconds_dt(atime: datetime) -> datetime:
|
||||
"""Round the time to the nearest second"""
|
||||
return (atime + timedelta(seconds=0.5)).replace(microsecond=0)
|
||||
|
||||
|
||||
def round_seconds_td(duration: timedelta) -> timedelta:
|
||||
"""Round the timedelta to the nearest second"""
|
||||
return timedelta(seconds=int(duration.total_seconds() + 0.5))
|
||||
|
||||
|
||||
class IUHistory:
|
||||
"""History access and caching"""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
|
||||
def __init__(self, hass: HomeAssistant, callback: Callable[[set[str]], None]):
|
||||
self._hass = hass
|
||||
self._callback = callback
|
||||
# Configuration variables
|
||||
self._history_span = timedelta(days=7)
|
||||
self._refresh_interval = timedelta(seconds=120)
|
||||
self._enabled = True
|
||||
# Private variables
|
||||
self._history_last: datetime = None
|
||||
self._cache: dict[str, Any] = {}
|
||||
self._entity_ids: list[str] = []
|
||||
self._refresh_remove: CALLBACK_TYPE = None
|
||||
self._stime: datetime = None
|
||||
self._initialised = False
|
||||
self._fixed_clock = True
|
||||
|
||||
def __del__(self):
|
||||
self._remove_refresh()
|
||||
|
||||
def _remove_refresh(self) -> None:
|
||||
"""Remove the scheduled refresh"""
|
||||
if self._refresh_remove is not None:
|
||||
self._refresh_remove()
|
||||
self._refresh_remove = None
|
||||
|
||||
def _get_next_refresh_event(self, utc_time: datetime, force: bool) -> datetime:
|
||||
"""Calculate the next event time."""
|
||||
if self._history_last is None or force or not self._fixed_clock:
|
||||
return utc_time
|
||||
return utc_time + self._refresh_interval
|
||||
|
||||
def _schedule_refresh(self, force: bool) -> None:
|
||||
"""Set up a listener for the next history refresh."""
|
||||
self._remove_refresh()
|
||||
self._history_last = self._get_next_refresh_event(dt.utcnow(), force)
|
||||
self._refresh_remove = async_track_point_in_utc_time(
|
||||
self._hass,
|
||||
self._async_handle_refresh_event,
|
||||
self._history_last,
|
||||
)
|
||||
|
||||
async def _async_handle_refresh_event(self, utc_time: datetime) -> None:
|
||||
"""Handle history event."""
|
||||
# pylint: disable=unused-argument
|
||||
self._refresh_remove = None
|
||||
if self._fixed_clock:
|
||||
self._schedule_refresh(False)
|
||||
await self._async_update_history(self._stime)
|
||||
|
||||
def _initialise(self) -> bool:
|
||||
"""Initialise this unit"""
|
||||
if self._initialised:
|
||||
return False
|
||||
|
||||
self._remove_refresh()
|
||||
self._history_last = None
|
||||
self._stime = None
|
||||
self._clear_cache()
|
||||
self._entity_ids.clear()
|
||||
for entity_id in self._hass.states.async_entity_ids():
|
||||
if entity_id.startswith(f"{BINARY_SENSOR}.{DOMAIN}_"):
|
||||
self._entity_ids.append(entity_id)
|
||||
self._initialised = True
|
||||
return True
|
||||
|
||||
def finalise(self):
|
||||
"""Finalise this unit"""
|
||||
self._remove_refresh()
|
||||
|
||||
def _clear_cache(self) -> None:
|
||||
self._cache = {}
|
||||
|
||||
def _today_duration(self, stime: datetime, data: list[State]) -> timedelta:
|
||||
"""Return the total on time"""
|
||||
# pylint: disable=no-self-use
|
||||
|
||||
elapsed = timedelta(0)
|
||||
front_marker: State = None
|
||||
start = midnight(stime)
|
||||
|
||||
for item in data:
|
||||
# Filter data
|
||||
if item.last_changed < start:
|
||||
continue
|
||||
if item.last_changed > stime:
|
||||
break
|
||||
|
||||
# Look for an on state
|
||||
if front_marker is None:
|
||||
if item.state == STATE_ON:
|
||||
front_marker = item
|
||||
continue
|
||||
|
||||
# Now look for an off state
|
||||
if item.state != STATE_ON:
|
||||
elapsed += item.last_changed - front_marker.last_changed
|
||||
front_marker = None
|
||||
|
||||
if front_marker is not None:
|
||||
elapsed += stime - front_marker.last_changed
|
||||
|
||||
return timedelta(seconds=round(elapsed.total_seconds()))
|
||||
|
||||
def _run_history(self, stime: datetime, data: list[State]) -> list:
|
||||
"""Return the on/off series"""
|
||||
# pylint: disable=no-self-use
|
||||
|
||||
def create_record(item: State, end: datetime) -> dict:
|
||||
result = OrderedDict()
|
||||
result[TIMELINE_START] = round_seconds_dt(item.last_changed)
|
||||
result[TIMELINE_END] = round_seconds_dt(end)
|
||||
result[TIMELINE_SCHEDULE_NAME] = item.attributes.get(ATTR_CURRENT_NAME)
|
||||
result[TIMELINE_ADJUSTMENT] = item.attributes.get(
|
||||
ATTR_CURRENT_ADJUSTMENT, ""
|
||||
)
|
||||
return result
|
||||
|
||||
run_history = []
|
||||
front_marker: State = None
|
||||
|
||||
for item in data:
|
||||
# Look for an on state
|
||||
if front_marker is None:
|
||||
if item.state == STATE_ON:
|
||||
front_marker = item
|
||||
continue
|
||||
|
||||
# Now look for an off state
|
||||
if item.state != STATE_ON:
|
||||
run_history.append(create_record(front_marker, item.last_changed))
|
||||
front_marker = None
|
||||
|
||||
if front_marker is not None:
|
||||
run_history.append(create_record(front_marker, stime))
|
||||
|
||||
return run_history
|
||||
|
||||
async def _async_update_history(self, stime: datetime) -> None:
|
||||
if len(self._entity_ids) == 0:
|
||||
return
|
||||
|
||||
start = self._stime - self._history_span
|
||||
if RECORDER_INSTANCE in self._hass.data:
|
||||
data = await get_instance(self._hass).async_add_executor_job(
|
||||
history.get_significant_states,
|
||||
self._hass,
|
||||
start,
|
||||
stime,
|
||||
self._entity_ids,
|
||||
None,
|
||||
True,
|
||||
False,
|
||||
)
|
||||
else:
|
||||
data = {}
|
||||
|
||||
if data is None or len(data) == 0:
|
||||
return
|
||||
|
||||
entity_ids: set[str] = set()
|
||||
for entity_id in data:
|
||||
new_run_history = self._run_history(stime, data[entity_id])
|
||||
new_today_on = self._today_duration(stime, data[entity_id])
|
||||
if entity_id not in self._cache:
|
||||
self._cache[entity_id] = {}
|
||||
elif (
|
||||
new_today_on == self._cache[entity_id][TODAY_ON]
|
||||
and new_run_history == self._cache[entity_id][TIMELINE]
|
||||
):
|
||||
continue
|
||||
self._cache[entity_id][TIMELINE] = new_run_history
|
||||
self._cache[entity_id][TODAY_ON] = new_today_on
|
||||
entity_ids.add(entity_id)
|
||||
if len(entity_ids) > 0:
|
||||
self._callback(entity_ids)
|
||||
|
||||
def load(self, config: OrderedDict, fixed_clock: bool) -> "IUHistory":
|
||||
"""Load config data"""
|
||||
if config is None:
|
||||
config = {}
|
||||
self._fixed_clock = fixed_clock
|
||||
|
||||
span_days: int = None
|
||||
refresh_seconds: int = None
|
||||
|
||||
# deprecated
|
||||
span_days = config.get(CONF_HISTORY_SPAN)
|
||||
refresh_seconds = config.get(CONF_HISTORY_REFRESH)
|
||||
|
||||
if CONF_HISTORY in config:
|
||||
hist_conf: dict = config[CONF_HISTORY]
|
||||
self._enabled = hist_conf.get(CONF_ENABLED, self._enabled)
|
||||
span_days = hist_conf.get(CONF_SPAN, span_days)
|
||||
refresh_seconds = hist_conf.get(CONF_REFRESH_INTERVAL, refresh_seconds)
|
||||
|
||||
if span_days is not None:
|
||||
self._history_span = timedelta(days=span_days)
|
||||
if refresh_seconds is not None:
|
||||
self._refresh_interval = timedelta(seconds=refresh_seconds)
|
||||
|
||||
self._initialised = False
|
||||
return self
|
||||
|
||||
def muster(self, stime: datetime, force: bool) -> None:
|
||||
"""Check and update history if required"""
|
||||
|
||||
if force:
|
||||
self._initialised = False
|
||||
|
||||
if not self._initialised:
|
||||
self._initialise()
|
||||
|
||||
if self._enabled and (
|
||||
force
|
||||
or self._stime is None
|
||||
or dt.as_local(self._stime).toordinal() != dt.as_local(stime).toordinal()
|
||||
or not self._fixed_clock
|
||||
):
|
||||
self._schedule_refresh(True)
|
||||
|
||||
self._stime = stime
|
||||
|
||||
def today_total(self, entity_id: str) -> timedelta:
|
||||
"""Return the total on time for today"""
|
||||
if entity_id in self._cache:
|
||||
return self._cache[entity_id][TODAY_ON]
|
||||
return timedelta(0)
|
||||
|
||||
def timeline(self, entity_id: str) -> list[dict]:
|
||||
"""Return the timeline history"""
|
||||
if entity_id in self._cache:
|
||||
return self._cache[entity_id][TIMELINE].copy()
|
||||
return []
|
||||
File diff suppressed because it is too large
Load Diff
19
config/custom_components/irrigation_unlimited/manifest.json
Normal file
19
config/custom_components/irrigation_unlimited/manifest.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"domain": "irrigation_unlimited",
|
||||
"name": "Irrigation Unlimited",
|
||||
"codeowners": [
|
||||
"@rgc99"
|
||||
],
|
||||
"config_flow": false,
|
||||
"dependencies": [
|
||||
"recorder",
|
||||
"history"
|
||||
],
|
||||
"documentation": "https://github.com/rgc99/irrigation_unlimited",
|
||||
"iot_class": "local_polling",
|
||||
"issue_tracker": "https://github.com/rgc99/irrigation_unlimited/issues",
|
||||
"requirements": [
|
||||
"crontab"
|
||||
],
|
||||
"version": "2024.5.0"
|
||||
}
|
||||
448
config/custom_components/irrigation_unlimited/schema.py
Normal file
448
config/custom_components/irrigation_unlimited/schema.py
Normal file
@@ -0,0 +1,448 @@
|
||||
"""This module holds the vaious schemas"""
|
||||
|
||||
from datetime import datetime, date
|
||||
import voluptuous as vol
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.const import (
|
||||
CONF_ENTITY_ID,
|
||||
CONF_AFTER,
|
||||
CONF_BEFORE,
|
||||
CONF_NAME,
|
||||
CONF_WEEKDAY,
|
||||
CONF_REPEAT,
|
||||
CONF_DELAY,
|
||||
CONF_FOR,
|
||||
CONF_UNTIL,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
CONF_ALLOW_MANUAL,
|
||||
CONF_CLOCK,
|
||||
CONF_ENABLED,
|
||||
CONF_FINISH,
|
||||
CONF_FIXED,
|
||||
CONF_FUTURE_SPAN,
|
||||
CONF_HISTORY,
|
||||
CONF_HISTORY_REFRESH,
|
||||
CONF_HISTORY_SPAN,
|
||||
CONF_MAX_LOG_ENTRIES,
|
||||
CONF_MAXIMUM,
|
||||
CONF_MINIMUM,
|
||||
CONF_MODE,
|
||||
CONF_MONTH,
|
||||
CONF_DAY,
|
||||
CONF_ODD,
|
||||
CONF_EVEN,
|
||||
CONF_RENAME_ENTITIES,
|
||||
CONF_RESULTS,
|
||||
CONF_SHOW_LOG,
|
||||
CONF_AUTOPLAY,
|
||||
CONF_ANCHOR,
|
||||
CONF_SPAN,
|
||||
CONF_SYNC_SWITCHES,
|
||||
CONF_SEER,
|
||||
CONF_CONTROLLERS,
|
||||
CONF_SCHEDULES,
|
||||
CONF_ZONES,
|
||||
CONF_DURATION,
|
||||
CONF_SUN,
|
||||
CONF_TIME,
|
||||
CONF_PREAMBLE,
|
||||
CONF_POSTAMBLE,
|
||||
CONF_GRANULARITY,
|
||||
CONF_TESTING,
|
||||
CONF_SPEED,
|
||||
CONF_TIMES,
|
||||
CONF_START,
|
||||
CONF_END,
|
||||
MONTHS,
|
||||
CONF_SHOW,
|
||||
CONF_CONFIG,
|
||||
CONF_TIMELINE,
|
||||
CONF_CONTROLLER_ID,
|
||||
CONF_ZONE_ID,
|
||||
CONF_SEQUENCES,
|
||||
CONF_ALL_ZONES_CONFIG,
|
||||
CONF_REFRESH_INTERVAL,
|
||||
CONF_OUTPUT_EVENTS,
|
||||
CONF_CRON,
|
||||
CONF_EVERY_N_DAYS,
|
||||
CONF_START_N_DAYS,
|
||||
CONF_CHECK_BACK,
|
||||
CONF_RETRIES,
|
||||
CONF_RESYNC,
|
||||
CONF_STATE_ON,
|
||||
CONF_STATE_OFF,
|
||||
CONF_PERCENTAGE,
|
||||
CONF_ACTUAL,
|
||||
CONF_INCREASE,
|
||||
CONF_DECREASE,
|
||||
CONF_RESET,
|
||||
CONF_SEQUENCE_ID,
|
||||
CONF_STATES,
|
||||
CONF_SCHEDULE_ID,
|
||||
CONF_FROM,
|
||||
CONF_VOLUME,
|
||||
CONF_VOLUME_PRECISION,
|
||||
CONF_VOLUME_SCALE,
|
||||
CONF_FLOW_RATE_PRECISION,
|
||||
CONF_FLOW_RATE_SCALE,
|
||||
CONF_QUEUE,
|
||||
CONF_QUEUE_MANUAL,
|
||||
CONF_USER,
|
||||
CONF_TOGGLE,
|
||||
CONF_EXTENDED_CONFIG,
|
||||
)
|
||||
|
||||
IU_ID = r"^[a-z0-9]+(_[a-z0-9]+)*$"
|
||||
|
||||
|
||||
def _list_is_not_empty(value):
|
||||
if value is None or len(value) < 1:
|
||||
raise vol.Invalid("Must have at least one entry")
|
||||
return value
|
||||
|
||||
|
||||
def _parse_dd_mmm(value: str) -> date | None:
|
||||
"""Convert a date string in dd mmm format to a date object."""
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d %b").date()
|
||||
|
||||
|
||||
USER_SCHEMA = vol.Schema(
|
||||
{},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
SHOW_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_CONFIG, False): cv.boolean,
|
||||
vol.Optional(CONF_TIMELINE, False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
SUN_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_SUN): cv.sun_event,
|
||||
vol.Optional(CONF_BEFORE): cv.positive_time_period,
|
||||
vol.Optional(CONF_AFTER): cv.positive_time_period,
|
||||
}
|
||||
)
|
||||
|
||||
CRON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_CRON): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
EVERY_N_DAYS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_EVERY_N_DAYS): cv.positive_int,
|
||||
vol.Required(CONF_START_N_DAYS): cv.date,
|
||||
}
|
||||
)
|
||||
|
||||
time_event = vol.Any(cv.time, SUN_SCHEMA, CRON_SCHEMA)
|
||||
anchor_event = vol.Any(CONF_START, CONF_FINISH)
|
||||
month_event = vol.All(cv.ensure_list, [vol.In(MONTHS)])
|
||||
|
||||
day_number = vol.All(vol.Coerce(int), vol.Range(min=0, max=31))
|
||||
day_event = vol.Any(
|
||||
CONF_ODD, CONF_EVEN, cv.ensure_list(day_number), EVERY_N_DAYS_SCHEMA
|
||||
)
|
||||
|
||||
SCHEDULE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_TIME): time_event,
|
||||
vol.Required(CONF_ANCHOR, default=CONF_START): anchor_event,
|
||||
vol.Required(CONF_DURATION): cv.positive_time_period,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_SCHEDULE_ID): cv.matches_regex(IU_ID),
|
||||
vol.Optional(CONF_WEEKDAY): cv.weekdays,
|
||||
vol.Optional(CONF_MONTH): month_event,
|
||||
vol.Optional(CONF_DAY): day_event,
|
||||
vol.Optional(CONF_ENABLED): cv.boolean,
|
||||
vol.Inclusive(CONF_FROM, "span"): _parse_dd_mmm,
|
||||
vol.Inclusive(CONF_UNTIL, "span"): _parse_dd_mmm,
|
||||
}
|
||||
)
|
||||
|
||||
SEQUENCE_SCHEDULE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_TIME): time_event,
|
||||
vol.Required(CONF_ANCHOR, default=CONF_START): anchor_event,
|
||||
vol.Optional(CONF_DURATION): cv.positive_time_period,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_SCHEDULE_ID): cv.matches_regex(IU_ID),
|
||||
vol.Optional(CONF_WEEKDAY): cv.weekdays,
|
||||
vol.Optional(CONF_MONTH): month_event,
|
||||
vol.Optional(CONF_DAY): day_event,
|
||||
vol.Optional(CONF_ENABLED): cv.boolean,
|
||||
vol.Inclusive(CONF_FROM, "span"): _parse_dd_mmm,
|
||||
vol.Inclusive(CONF_UNTIL, "span"): _parse_dd_mmm,
|
||||
}
|
||||
)
|
||||
|
||||
LOAD_SCHEDULE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_SCHEDULE_ID): cv.matches_regex(IU_ID),
|
||||
vol.Optional(CONF_TIME): time_event,
|
||||
vol.Optional(CONF_ANCHOR): anchor_event,
|
||||
vol.Optional(CONF_DURATION): cv.positive_time_period_template,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_WEEKDAY): cv.weekdays,
|
||||
vol.Optional(CONF_MONTH): month_event,
|
||||
vol.Optional(CONF_DAY): day_event,
|
||||
vol.Optional(CONF_ENABLED): cv.boolean,
|
||||
vol.Inclusive(CONF_FROM, "span"): _parse_dd_mmm,
|
||||
vol.Inclusive(CONF_UNTIL, "span"): _parse_dd_mmm,
|
||||
}
|
||||
)
|
||||
|
||||
CHECK_BACK_SCHEMA = vol.All(
|
||||
cv.deprecated(CONF_STATE_ON),
|
||||
cv.deprecated(CONF_STATE_OFF),
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_STATES): vol.Any("none", "all", "on", "off"),
|
||||
vol.Optional(CONF_DELAY): cv.positive_int,
|
||||
vol.Optional(CONF_RETRIES): cv.positive_int,
|
||||
vol.Optional(CONF_RESYNC): cv.boolean,
|
||||
vol.Optional(CONF_STATE_ON): cv.string, # Deprecated
|
||||
vol.Optional(CONF_STATE_OFF): cv.string, # Deprecated
|
||||
vol.Optional(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Optional(CONF_TOGGLE): cv.boolean,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
VOLUME_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Optional(CONF_VOLUME_PRECISION): cv.positive_int,
|
||||
vol.Optional(CONF_VOLUME_SCALE): cv.positive_float,
|
||||
vol.Optional(CONF_FLOW_RATE_PRECISION): cv.positive_int,
|
||||
vol.Optional(CONF_FLOW_RATE_SCALE): cv.positive_float,
|
||||
}
|
||||
)
|
||||
|
||||
ZONE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_SCHEDULES): vol.All(cv.ensure_list, [SCHEDULE_SCHEMA]),
|
||||
vol.Optional(CONF_ZONE_ID): cv.matches_regex(IU_ID),
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(CONF_ENABLED): cv.boolean,
|
||||
vol.Optional(CONF_ALLOW_MANUAL): cv.boolean,
|
||||
vol.Optional(CONF_MINIMUM): cv.positive_time_period,
|
||||
vol.Optional(CONF_MAXIMUM): cv.positive_time_period,
|
||||
vol.Optional(CONF_FUTURE_SPAN): cv.positive_int,
|
||||
vol.Optional(CONF_SHOW): vol.All(SHOW_SCHEMA),
|
||||
vol.Optional(CONF_CHECK_BACK): vol.All(CHECK_BACK_SCHEMA),
|
||||
vol.Optional(CONF_VOLUME): vol.All(VOLUME_SCHEMA),
|
||||
vol.Optional(CONF_DURATION): cv.positive_time_period_template,
|
||||
vol.Optional(CONF_USER): vol.All(USER_SCHEMA),
|
||||
}
|
||||
)
|
||||
|
||||
ALL_ZONES_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_SHOW): vol.All(SHOW_SCHEMA),
|
||||
vol.Optional(CONF_MINIMUM): cv.positive_time_period,
|
||||
vol.Optional(CONF_MAXIMUM): cv.positive_time_period,
|
||||
vol.Optional(CONF_FUTURE_SPAN): cv.positive_int,
|
||||
vol.Optional(CONF_ALLOW_MANUAL): cv.boolean,
|
||||
vol.Optional(CONF_CHECK_BACK): vol.All(CHECK_BACK_SCHEMA),
|
||||
vol.Optional(CONF_VOLUME): vol.All(VOLUME_SCHEMA),
|
||||
vol.Optional(CONF_DURATION): cv.positive_time_period_template,
|
||||
vol.Optional(CONF_USER): vol.All(USER_SCHEMA),
|
||||
}
|
||||
)
|
||||
|
||||
SEQUENCE_ZONE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ZONE_ID): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_DELAY): cv.time_period,
|
||||
vol.Optional(CONF_DURATION): cv.positive_time_period,
|
||||
vol.Optional(CONF_REPEAT): cv.positive_int,
|
||||
vol.Optional(CONF_ENABLED): cv.boolean,
|
||||
vol.Optional(CONF_VOLUME): cv.positive_float,
|
||||
}
|
||||
)
|
||||
|
||||
SEQUENCE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ZONES, default={}): vol.All(
|
||||
cv.ensure_list, [SEQUENCE_ZONE_SCHEMA], _list_is_not_empty
|
||||
),
|
||||
vol.Optional(CONF_SCHEDULES): vol.All(
|
||||
cv.ensure_list, [SEQUENCE_SCHEDULE_SCHEMA]
|
||||
),
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_DELAY): cv.time_period,
|
||||
vol.Optional(CONF_DURATION): cv.positive_time_period,
|
||||
vol.Optional(CONF_REPEAT): cv.positive_int,
|
||||
vol.Optional(CONF_ENABLED): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
CONTROLLER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ZONES): vol.All(
|
||||
cv.ensure_list, [ZONE_SCHEMA], _list_is_not_empty
|
||||
),
|
||||
vol.Optional(CONF_SEQUENCES): vol.All(
|
||||
cv.ensure_list, [SEQUENCE_SCHEMA], _list_is_not_empty
|
||||
),
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_CONTROLLER_ID): cv.matches_regex(IU_ID),
|
||||
vol.Optional(CONF_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(CONF_PREAMBLE): cv.time_period,
|
||||
vol.Optional(CONF_POSTAMBLE): cv.time_period,
|
||||
vol.Optional(CONF_ENABLED): cv.boolean,
|
||||
vol.Optional(CONF_ALL_ZONES_CONFIG): vol.All(ALL_ZONES_SCHEMA),
|
||||
vol.Optional(CONF_QUEUE_MANUAL): cv.boolean,
|
||||
vol.Optional(CONF_CHECK_BACK): vol.All(CHECK_BACK_SCHEMA),
|
||||
vol.Optional(CONF_VOLUME): vol.All(VOLUME_SCHEMA),
|
||||
vol.Optional(CONF_USER): vol.All(USER_SCHEMA),
|
||||
}
|
||||
)
|
||||
|
||||
HISTORY_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_ENABLED): cv.boolean,
|
||||
vol.Optional(CONF_REFRESH_INTERVAL): cv.positive_int,
|
||||
vol.Optional(CONF_SPAN): cv.positive_int,
|
||||
}
|
||||
)
|
||||
|
||||
clock_mode = vol.Any(CONF_FIXED, CONF_SEER)
|
||||
|
||||
CLOCK_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_MODE, default=CONF_SEER): clock_mode,
|
||||
vol.Optional(CONF_SHOW_LOG, default=False): cv.boolean,
|
||||
vol.Optional(CONF_MAX_LOG_ENTRIES): cv.positive_int,
|
||||
}
|
||||
)
|
||||
|
||||
TEST_RESULT_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("t"): cv.datetime,
|
||||
vol.Required("c"): cv.positive_int,
|
||||
vol.Required("z"): cv.positive_int,
|
||||
vol.Required("s"): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
TEST_TIME_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_START): cv.datetime,
|
||||
vol.Required(CONF_END): cv.datetime,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_RESULTS): [TEST_RESULT_SCHEMA],
|
||||
}
|
||||
)
|
||||
|
||||
TEST_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_ENABLED): cv.boolean,
|
||||
vol.Optional(CONF_SPEED): cv.positive_float,
|
||||
vol.Optional(CONF_TIMES): [TEST_TIME_SCHEMA],
|
||||
vol.Optional(CONF_OUTPUT_EVENTS): cv.boolean,
|
||||
vol.Optional(CONF_SHOW_LOG): cv.boolean,
|
||||
vol.Optional(CONF_AUTOPLAY): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
IRRIGATION_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_CONTROLLERS, default={}): vol.All(
|
||||
cv.ensure_list, [CONTROLLER_SCHEMA], _list_is_not_empty
|
||||
),
|
||||
vol.Optional(CONF_GRANULARITY): cv.positive_int,
|
||||
vol.Optional(CONF_REFRESH_INTERVAL): cv.positive_int,
|
||||
vol.Optional(CONF_HISTORY_SPAN): cv.positive_int,
|
||||
vol.Optional(CONF_HISTORY_REFRESH): cv.positive_int,
|
||||
vol.Optional(CONF_SYNC_SWITCHES): cv.boolean,
|
||||
vol.Optional(CONF_RENAME_ENTITIES): cv.boolean,
|
||||
vol.Optional(CONF_TESTING): TEST_SCHEMA,
|
||||
vol.Optional(CONF_HISTORY): HISTORY_SCHEMA,
|
||||
vol.Optional(CONF_CLOCK): CLOCK_SCHEMA,
|
||||
vol.Optional(CONF_EXTENDED_CONFIG): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
positive_float_template = vol.Any(cv.positive_float, cv.template)
|
||||
|
||||
ENTITY_SCHEMA = {vol.Required(CONF_ENTITY_ID): cv.entity_id}
|
||||
|
||||
ENABLE_DISABLE_SCHEMA = {
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(CONF_ZONES): cv.ensure_list,
|
||||
vol.Optional(CONF_SEQUENCE_ID): cv.positive_int,
|
||||
}
|
||||
|
||||
TIME_ADJUST_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
||||
vol.Exclusive(
|
||||
CONF_ACTUAL, "adjust_method"
|
||||
): cv.positive_time_period_template,
|
||||
vol.Exclusive(CONF_PERCENTAGE, "adjust_method"): positive_float_template,
|
||||
vol.Exclusive(
|
||||
CONF_INCREASE, "adjust_method"
|
||||
): cv.positive_time_period_template,
|
||||
vol.Exclusive(
|
||||
CONF_DECREASE, "adjust_method"
|
||||
): cv.positive_time_period_template,
|
||||
vol.Exclusive(CONF_RESET, "adjust_method"): None,
|
||||
vol.Optional(CONF_MINIMUM): cv.positive_time_period_template,
|
||||
vol.Optional(CONF_MAXIMUM): cv.positive_time_period_template,
|
||||
vol.Optional(CONF_ZONES): cv.ensure_list,
|
||||
vol.Optional(CONF_SEQUENCE_ID): cv.positive_int,
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key(
|
||||
CONF_ACTUAL, CONF_PERCENTAGE, CONF_INCREASE, CONF_DECREASE, CONF_RESET
|
||||
),
|
||||
)
|
||||
|
||||
MANUAL_RUN_SCHEMA = {
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(CONF_TIME): cv.positive_time_period_template,
|
||||
vol.Optional(CONF_DELAY): cv.time_period,
|
||||
vol.Optional(CONF_QUEUE): cv.boolean,
|
||||
vol.Optional(CONF_ZONES): cv.ensure_list,
|
||||
vol.Optional(CONF_SEQUENCE_ID): cv.positive_int,
|
||||
}
|
||||
|
||||
SUSPEND_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
||||
vol.Exclusive(CONF_FOR, "time_method"): cv.positive_time_period_template,
|
||||
vol.Exclusive(CONF_UNTIL, "time_method"): cv.datetime,
|
||||
vol.Exclusive(CONF_RESET, "time_method"): None,
|
||||
vol.Optional(CONF_ZONES): cv.ensure_list,
|
||||
vol.Optional(CONF_SEQUENCE_ID): cv.positive_int,
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key(CONF_FOR, CONF_UNTIL, CONF_RESET),
|
||||
)
|
||||
|
||||
CANCEL_SCHEMA = {
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(CONF_ZONES): cv.ensure_list,
|
||||
vol.Optional(CONF_SEQUENCE_ID): cv.positive_int,
|
||||
}
|
||||
|
||||
PAUSE_RESUME_SCHEMA = {
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(CONF_SEQUENCE_ID): cv.positive_int,
|
||||
}
|
||||
|
||||
RELOAD_SERVICE_SCHEMA = vol.Schema({})
|
||||
123
config/custom_components/irrigation_unlimited/service.py
Normal file
123
config/custom_components/irrigation_unlimited/service.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""This module handles the HA service call interface"""
|
||||
from homeassistant.core import ServiceCall, callback
|
||||
from homeassistant.util import dt
|
||||
from homeassistant.helpers import entity_platform
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.service import async_register_admin_service
|
||||
from homeassistant.const import (
|
||||
SERVICE_RELOAD,
|
||||
)
|
||||
|
||||
from .irrigation_unlimited import IUCoordinator
|
||||
from .entity import IUEntity
|
||||
from .schema import (
|
||||
ENTITY_SCHEMA,
|
||||
ENABLE_DISABLE_SCHEMA,
|
||||
TIME_ADJUST_SCHEMA,
|
||||
MANUAL_RUN_SCHEMA,
|
||||
RELOAD_SERVICE_SCHEMA,
|
||||
LOAD_SCHEDULE_SCHEMA,
|
||||
SUSPEND_SCHEMA,
|
||||
CANCEL_SCHEMA,
|
||||
PAUSE_RESUME_SCHEMA,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
SERVICE_CANCEL,
|
||||
SERVICE_DISABLE,
|
||||
SERVICE_ENABLE,
|
||||
SERVICE_MANUAL_RUN,
|
||||
SERVICE_TIME_ADJUST,
|
||||
SERVICE_TOGGLE,
|
||||
SERVICE_LOAD_SCHEDULE,
|
||||
SERVICE_SUSPEND,
|
||||
SERVICE_SKIP,
|
||||
SERVICE_PAUSE,
|
||||
SERVICE_RESUME,
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
async def async_entity_service_handler(entity: IUEntity, call: ServiceCall) -> None:
|
||||
"""Dispatch the service call"""
|
||||
entity.dispatch(call.service, call)
|
||||
|
||||
|
||||
def register_platform_services(platform: entity_platform.EntityPlatform) -> None:
|
||||
"""Register all the available service calls for the entities"""
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_ENABLE, ENABLE_DISABLE_SCHEMA, async_entity_service_handler
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_DISABLE, ENABLE_DISABLE_SCHEMA, async_entity_service_handler
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_TOGGLE, ENABLE_DISABLE_SCHEMA, async_entity_service_handler
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_CANCEL, CANCEL_SCHEMA, async_entity_service_handler
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_TIME_ADJUST, TIME_ADJUST_SCHEMA, async_entity_service_handler
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_MANUAL_RUN, MANUAL_RUN_SCHEMA, async_entity_service_handler
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SUSPEND, SUSPEND_SCHEMA, async_entity_service_handler
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SKIP, ENTITY_SCHEMA, async_entity_service_handler
|
||||
)
|
||||
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_PAUSE, PAUSE_RESUME_SCHEMA, async_entity_service_handler
|
||||
)
|
||||
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_RESUME, PAUSE_RESUME_SCHEMA, async_entity_service_handler
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
def register_component_services(
|
||||
component: EntityComponent, coordinator: IUCoordinator
|
||||
) -> None:
|
||||
"""Register the component"""
|
||||
|
||||
@callback
|
||||
async def reload_service_handler(call: ServiceCall) -> None:
|
||||
"""Reload yaml entities."""
|
||||
# pylint: disable=unused-argument
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from .binary_sensor import async_reload_platform
|
||||
|
||||
conf = await component.async_prepare_reload(skip_reset=True)
|
||||
if conf is None or conf == {}:
|
||||
conf = {DOMAIN: {}}
|
||||
coordinator.load(conf[DOMAIN])
|
||||
await async_reload_platform(component, coordinator)
|
||||
coordinator.timer(dt.utcnow())
|
||||
coordinator.clock.start()
|
||||
|
||||
async_register_admin_service(
|
||||
component.hass,
|
||||
DOMAIN,
|
||||
SERVICE_RELOAD,
|
||||
reload_service_handler,
|
||||
schema=RELOAD_SERVICE_SCHEMA,
|
||||
)
|
||||
|
||||
@callback
|
||||
async def load_schedule_service_handler(call: ServiceCall) -> None:
|
||||
"""Reload schedule."""
|
||||
coordinator.service_call(call.service, None, None, None, call.data)
|
||||
|
||||
component.hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_LOAD_SCHEDULE,
|
||||
load_schedule_service_handler,
|
||||
LOAD_SCHEDULE_SCHEMA,
|
||||
)
|
||||
506
config/custom_components/irrigation_unlimited/services.yaml
Normal file
506
config/custom_components/irrigation_unlimited/services.yaml
Normal file
@@ -0,0 +1,506 @@
|
||||
# Describes the format for available Irrigation Unlimited services
|
||||
|
||||
enable:
|
||||
name: Enable
|
||||
description: Enable the controller or zone.
|
||||
fields:
|
||||
entity_id:
|
||||
name: Entity Id
|
||||
description: Name of the Irrigation Unlimited entity.
|
||||
example: "binary_sensor.irrigation_unlimited_c1_z1"
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
integration: irrigation_unlimited
|
||||
domain: binary_sensor
|
||||
|
||||
sequence_id:
|
||||
name: Sequence Id
|
||||
description: Id of the sequence to enable (entity_id should be the controller).
|
||||
example: 1
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 9999
|
||||
mode: box
|
||||
|
||||
zones:
|
||||
name: Zones
|
||||
description: Id(s) of the zone
|
||||
example: 1
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 9999
|
||||
mode: box
|
||||
|
||||
disable:
|
||||
name: Disable
|
||||
description: Disable the controller or zone
|
||||
fields:
|
||||
entity_id:
|
||||
name: Entity Id
|
||||
description: Name of the Irrigation Unlimited entity.
|
||||
example: "binary_sensor.irrigation_unlimited_c1_z1"
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
integration: irrigation_unlimited
|
||||
domain: binary_sensor
|
||||
|
||||
sequence_id:
|
||||
name: Sequence Id
|
||||
description: Id of the sequence to disable (entity_id should be the controller).
|
||||
example: 1
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 9999
|
||||
mode: box
|
||||
|
||||
zones:
|
||||
name: Zones
|
||||
description: Id(s) of the zone
|
||||
example: 1
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 9999
|
||||
mode: box
|
||||
|
||||
toggle:
|
||||
name: Toggle
|
||||
description: Toggle the enable/disable status of controller or zone.
|
||||
fields:
|
||||
entity_id:
|
||||
name: Entity Id
|
||||
description: Name of the Irrigation Unlimited entity.
|
||||
example: "binary_sensor.irrigation_unlimited_c1_z1"
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
integration: irrigation_unlimited
|
||||
domain: binary_sensor
|
||||
|
||||
sequence_id:
|
||||
name: Sequence Id
|
||||
description: Id of the sequence to toggle (entity_id should be the controller).
|
||||
example: 1
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 9999
|
||||
mode: box
|
||||
|
||||
zones:
|
||||
name: Zones
|
||||
description: Id(s) of the zone
|
||||
example: 1
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 9999
|
||||
mode: box
|
||||
|
||||
suspend:
|
||||
name: Suspend
|
||||
description: Suspend the controller or zone
|
||||
fields:
|
||||
entity_id:
|
||||
name: Entity Id
|
||||
description: Name of the Irrigation Unlimited entity.
|
||||
example: "binary_sensor.irrigation_unlimited_c1_z1"
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
integration: irrigation_unlimited
|
||||
domain: binary_sensor
|
||||
|
||||
sequence_id:
|
||||
name: Sequence Id
|
||||
description: Id of the sequence to suspend (entity_id should be the controller).
|
||||
example: 1
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 9999
|
||||
mode: box
|
||||
|
||||
zones:
|
||||
name: Zones
|
||||
description: Id(s) of the zone
|
||||
example: 1
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 9999
|
||||
mode: box
|
||||
|
||||
for:
|
||||
name: For
|
||||
description: The amount of time to suspend
|
||||
example: "01:00"
|
||||
required: false
|
||||
selector:
|
||||
duration:
|
||||
|
||||
until:
|
||||
name: Until
|
||||
description: The date and time to restart
|
||||
example: "2023-01-01 10:00"
|
||||
required: false
|
||||
selector:
|
||||
datetime:
|
||||
|
||||
reset:
|
||||
name: Reset
|
||||
description: Cancel any suspension
|
||||
example: ""
|
||||
required: false
|
||||
|
||||
cancel:
|
||||
name: Cancel
|
||||
description: Cancel the current run.
|
||||
fields:
|
||||
entity_id:
|
||||
name: Entity Id
|
||||
description: Name of the Irrigation Unlimited entity.
|
||||
example: "binary_sensor.irrigation_unlimited_c1_z1"
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
integration: irrigation_unlimited
|
||||
domain: binary_sensor
|
||||
|
||||
sequence_id:
|
||||
name: Sequence Id
|
||||
description: Id of the sequence to cancel (entity_id should be the controller).
|
||||
example: 1
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 9999
|
||||
mode: box
|
||||
|
||||
zones:
|
||||
name: Zones
|
||||
description: Id(s) of the zone
|
||||
example: 1
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 9999
|
||||
mode: box
|
||||
|
||||
adjust_time:
|
||||
name: Adjust time
|
||||
description: Adjust the run times.
|
||||
fields:
|
||||
entity_id:
|
||||
name: Entity Id
|
||||
description: Name of the Irrigation Unlimited entity.
|
||||
example: "binary_sensor.irrigation_unlimited_c1_z1"
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
integration: irrigation_unlimited
|
||||
domain: binary_sensor
|
||||
|
||||
sequence_id:
|
||||
name: Sequence Id
|
||||
description: Id of the sequence to adjust (entity_id should be the controller).
|
||||
example: 1
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 9999
|
||||
mode: box
|
||||
|
||||
zones:
|
||||
name: Zones
|
||||
description: Id of the zone
|
||||
example: 1
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 9999
|
||||
mode: box
|
||||
|
||||
reset:
|
||||
name: Reset
|
||||
description: Revert to original schedule
|
||||
example: ""
|
||||
required: false
|
||||
|
||||
percentage:
|
||||
name: Percentage
|
||||
description: Adjust the run time by a percentage.
|
||||
example: "150"
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 1000
|
||||
unit_of_measurement: "%"
|
||||
|
||||
actual:
|
||||
name: Actual
|
||||
description: Set a new run time.
|
||||
example: "00:15"
|
||||
required: false
|
||||
selector:
|
||||
duration:
|
||||
|
||||
increase:
|
||||
name: Increase
|
||||
description: Increase the run time.
|
||||
example: "00:02"
|
||||
required: false
|
||||
selector:
|
||||
duration:
|
||||
|
||||
decrease:
|
||||
name: Decrease
|
||||
description: Decrease the run time.
|
||||
example: "00:02"
|
||||
required: false
|
||||
selector:
|
||||
duration:
|
||||
|
||||
minimum:
|
||||
name: Minimum
|
||||
description: Minimum run time.
|
||||
example: "00:01"
|
||||
required: false
|
||||
selector:
|
||||
duration:
|
||||
|
||||
maximum:
|
||||
name: Maximum
|
||||
description: Maximum run time.
|
||||
example: "01:00"
|
||||
required: false
|
||||
selector:
|
||||
duration:
|
||||
|
||||
manual_run:
|
||||
name: Manual run
|
||||
description: Turn on immediately for a set period.
|
||||
fields:
|
||||
entity_id:
|
||||
name: Entity Id
|
||||
description: Name of the Irrigation Unlimited entity.
|
||||
example: "binary_sensor.irrigation_unlimited_c1_z1"
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
integration: irrigation_unlimited
|
||||
domain: binary_sensor
|
||||
|
||||
time:
|
||||
name: Time
|
||||
description: The amount of time to run.
|
||||
example: "00:10"
|
||||
required: false
|
||||
selector:
|
||||
duration:
|
||||
|
||||
delay:
|
||||
name: Delay
|
||||
description: Delay between queued runs.
|
||||
example: "00:00:30"
|
||||
required: false
|
||||
selector:
|
||||
duration:
|
||||
|
||||
queue:
|
||||
name: Queue
|
||||
description: Start immediately or queue run.
|
||||
example: false
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
sequence_id:
|
||||
name: Sequence Id
|
||||
description: Id of the sequence to run.
|
||||
example: 1
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 9999
|
||||
mode: box
|
||||
|
||||
pause:
|
||||
name: Pause
|
||||
description: Pause a running sequence
|
||||
fields:
|
||||
entity_id:
|
||||
name: Entity Id
|
||||
description: Name of the Irrigation Unlimited controller or sequence entity.
|
||||
example: "binary_sensor.irrigation_unlimited_c1_s1"
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
integration: irrigation_unlimited
|
||||
domain: binary_sensor
|
||||
|
||||
sequence_id:
|
||||
name: Sequence Id
|
||||
description: Used when entity_id is the controller. Id of the sequence to pause (0=all sequences)
|
||||
example: 1
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 9999
|
||||
mode: box
|
||||
|
||||
resume:
|
||||
name: Resume
|
||||
description: Resume a paused sequence
|
||||
fields:
|
||||
entity_id:
|
||||
name: Entity Id
|
||||
description: Name of the Irrigation Unlimited controller or sequence entity.
|
||||
example: "binary_sensor.irrigation_unlimited_c1_s1"
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
integration: irrigation_unlimited
|
||||
domain: binary_sensor
|
||||
|
||||
sequence_id:
|
||||
name: Sequence Id
|
||||
description: Used when entity_id is the controller. Id of the sequence to resume (0=all sequences).
|
||||
example: 1
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 9999
|
||||
mode: box
|
||||
|
||||
reload:
|
||||
name: Reload
|
||||
description: Reload the configuration
|
||||
|
||||
load_schedule:
|
||||
name: Load schedule
|
||||
description: Load a schedule.
|
||||
fields:
|
||||
schedule_id:
|
||||
name: Schedule Id
|
||||
description: Id of the schedule
|
||||
example: schedule_1
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
time:
|
||||
name: Time
|
||||
description: Time of day
|
||||
example: '06:30'
|
||||
required: false
|
||||
selector:
|
||||
time:
|
||||
anchor:
|
||||
name: Anchor
|
||||
description: Start or finish at the specified time
|
||||
example: start
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- start
|
||||
- finish
|
||||
duration:
|
||||
name: Duration
|
||||
description: The length of time to run
|
||||
example: '00:10:00'
|
||||
required: false
|
||||
selector:
|
||||
duration:
|
||||
name:
|
||||
name: Name
|
||||
description: Friendly name of the schedule
|
||||
example: Sunrise
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
weekday:
|
||||
name: Weekday
|
||||
description: Days of week to run
|
||||
example: mon
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
multiple: true
|
||||
options:
|
||||
- mon
|
||||
- tue
|
||||
- wed
|
||||
- thu
|
||||
- fri
|
||||
- sat
|
||||
- sun
|
||||
month:
|
||||
name: Month
|
||||
description: Months to run
|
||||
example: jan
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
multiple: true
|
||||
options:
|
||||
- jan
|
||||
- feb
|
||||
- mar
|
||||
- apr
|
||||
- may
|
||||
- jun
|
||||
- jul
|
||||
- aug
|
||||
- sep
|
||||
- oct
|
||||
- nov
|
||||
- dec
|
||||
day:
|
||||
name: Day
|
||||
description: Day of month to run
|
||||
example: 1
|
||||
required: false
|
||||
selector:
|
||||
object:
|
||||
from:
|
||||
name: From
|
||||
description: Start date in year
|
||||
example: 15 apr
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
until:
|
||||
name: Until
|
||||
description: End date in year
|
||||
example: 15 sep
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
enabled:
|
||||
name: Enabled
|
||||
description: Enable/disable this schedule
|
||||
example: true
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Blueprint",
|
||||
"description": "If you need help with the configuration have a look here: https://github.com/custom-components/integration_blueprint",
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"auth": "Username/Password is wrong."
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "Only a single instance is allowed."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"binary_sensor": "Binary sensor enabled",
|
||||
"sensor": "Sensor enabled",
|
||||
"switch": "Switch enabled"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Blueprint",
|
||||
"description": "Si necesitas ayuda con la configuracion hecha un vistazo aqui: https://github.com/custom-components/integration_blueprint",
|
||||
"data": {
|
||||
"username": "Usuario",
|
||||
"password": "Password"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"auth": "Usuario/Password incorrecto."
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "Solo se permite una instancia."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"binary_sensor": "Binary sensor habilitado",
|
||||
"sensor": "Sensor habilitado",
|
||||
"switch": "Switch habilitado"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Blueprint",
|
||||
"description": "Si vous avez besoin d'aide pour la configuration, regardez ici: https://github.com/custom-components/integration_blueprint",
|
||||
"data": {
|
||||
"username": "Identifiant",
|
||||
"password": "Mot de Passe"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"auth": "Identifiant ou mot de passe erroné."
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "Une seule instance est autorisée."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"binary_sensor": "Capteur binaire activé",
|
||||
"sensor": "Capteur activé",
|
||||
"switch": "Interrupteur activé"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Blueprint",
|
||||
"description": "Hvis du trenger hjep til konfigurasjon ta en titt her: https://github.com/custom-components/integration_blueprint",
|
||||
"data": {
|
||||
"username": "Brukernavn",
|
||||
"password": "Passord"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"auth": "Brukernavn/Passord er feil."
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "Denne integrasjonen kan kun konfigureres en gang."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"binary_sensor": "Binær sensor aktivert",
|
||||
"sensor": "Sensor aktivert",
|
||||
"switch": "Bryter aktivert"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user