Home Assistant Git Exporter

This commit is contained in:
root
2024-05-31 09:39:52 +02:00
parent cd6fa93633
commit d5ccfbb540
1353 changed files with 43876 additions and 0 deletions

View 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

View 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

View 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}
-------------------------------------------------------------------
"""

View 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

View 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

View 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"
}

View 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({})

View 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,
)

View 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:

View File

@@ -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"
}
}
}
}
}

View File

@@ -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"
}
}
}
}
}

View File

@@ -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é"
}
}
}
}
}

View File

@@ -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"
}
}
}
}
}