Skip to content

Commit

Permalink
Add sequences feature.
Browse files Browse the repository at this point in the history
  • Loading branch information
rgc99 committed Mar 20, 2021
1 parent 68bc6ab commit 9fe619c
Show file tree
Hide file tree
Showing 7 changed files with 489 additions and 151 deletions.
59 changes: 57 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ Zones also have an associated sensor which, like the master, shows on/off status
1. Unlimited controllers.
2. Unlimited zones.
3. Unlimited schedules. Schedule by absolute time or sun events (sunrise/sunset). Select by days of the week (mon/tue/wed...). Select by days in the month (1/2/3.../odd/even). Select by months in the year (jan/feb/mar...). Overlapped schedules.
4. Hardware independant. Use your own switches/valve controllers.
5. Software independant. Pure play python.
4. Unlimited sequences. Operate zones one at a time in a particular order with a delay in between.
5. Hardware independant. Use your own switches/valve controllers.
6. Software independant. Pure play python.

*Practicle limitations will depend on your hardware.

Expand Down Expand Up @@ -113,6 +114,7 @@ This is the controller or master object and manages a collection of zones. There
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| `zones` | list | _[Zone Objects](#zone-objects)_ | Zone details (Must have at least one) |
| `sequences` | list | _[Sequence Objects](#sequence-objects)_ | Sequence details |
| `name` | string | Controller _N_ | Friendly name for the controller |
| `enabled` | bool | true | Enable/disable the controller |
| `preamble` | time | '00:00' | The time master turns on before any zone turns on |
Expand All @@ -126,6 +128,7 @@ The zone object manages a collection of schedules. There must be at least one zo
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| `schedules` | list | _[Schedule Objects](#schedule-objects)_ | Schedule details (Must have at least one) |
| `zone_id` | string | _N_ | Zone reference. Used for sequencing. |
| `name` | string | Zone _N_ | Friendly name for the zone |
| `enabled` | bool | true | Enable/disable the zone |
| `minimum` | time | '00:01' | The minimum run time |
Expand Down Expand Up @@ -157,6 +160,26 @@ Leave the time value in the _[Schedule Objects](#schedule-objects)_ blank and ad
| `before` | time | '00:00' | Time before the event |
| `after` | time | '00:00' | Time after the event |

### Sequence Objects

Sequences allow zones to run one at a time in a particular order with a delay in between.

| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| `schedules` | list | _[Schedule Objects](#schedule-objects)_ | Schedule details (Must have at least one). Note: `duration` is ignored |
| `zones` | list | _[Sequence Zone Objects](#sequence-zone-objects)_ | Zone details (Must have at least one) |
| `delay` | time | **Required** | Delay between zones |
| `name` | string | Run _N_ | Friendly name for the sequence |

### Sequence Zone Objects

The sequence zone is a reference to the actual zone defined in the _[Zone Objects](#zone-objects)_. Ensure the `zone_id`'s match between this object and the zone object. The zone may appear more than once in the case of a split run.

| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| `zone_id` | string | **Required** | Zone reference. This must match the `zone_id` in the _[Zone Objects](#zone-objects)_ |
| `duration` | time | **Required** | The length of time to run |

### Testing Object

The testing object is useful for running through a predetermined regime. Note: the `speed` value does _not_ alter the system clock in any way. It is accomplished by an internal 'virtual clock'.
Expand Down Expand Up @@ -208,6 +231,38 @@ irrigation_unlimited:
duration: '00:30'
~~~

### Sequence example

~~~yaml
# Example configuration.yaml entry
irrigation_unlimited:
controllers:
zones:
- name: "Front lawn"
entity_id: "switch.my_switch_1"
- name: "Vege patch"
entity_id: "switch.my_switch_2"
- name: "Flower bed"
entity_id: "switch.my_switch_3"
sequences:
- delay: "00:01"
schedules:
- name: "Sunrise"
time:
sun: "sunrise"
- name: "After sunset"
time:
sun: "sunset"
after: "00:30"
zones:
- zone_id: 1
duration: "00:10"
- zone_id: 2
duration: "00:02"
- zone_id: 3
duration: "00:01"
~~~

For a more comprehensive example refer to [here](./examples/all_the_bells_and_whistles.yaml).

## Services
Expand Down
62 changes: 59 additions & 3 deletions custom_components/irrigation_unlimited/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
https://github.com/rgc99/irrigation_unlimited
"""
import logging
from typing import Optional
import voluptuous as vol
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.core import Config, HomeAssistant
Expand All @@ -14,6 +15,8 @@
CONF_ENTITY_ID,
CONF_NAME,
CONF_WEEKDAY,
CONF_DELAY,
CONF_ID,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
Expand Down Expand Up @@ -50,6 +53,12 @@
CONF_START,
CONF_END,
MONTHS,
CONF_SHOW,
CONF_CONFIG,
CONF_TIMELINE,
CONF_ZONE_ID,
CONF_SEQUENCES,
CONF_ALL_ZONES_CONFIG,
)

_LOGGER: logging.Logger = logging.getLogger(__package__)
Expand All @@ -61,6 +70,13 @@ def _list_is_not_empty(value):
return value


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,
Expand Down Expand Up @@ -88,14 +104,50 @@ def _list_is_not_empty(value):

ZONE_SCHEMA = vol.Schema(
{
vol.Required(CONF_SCHEDULES, default={}): vol.All(
cv.ensure_list, [SCHEDULE_SCHEMA], _list_is_not_empty
),
vol.Optional(CONF_SCHEDULES): vol.All(cv.ensure_list, [SCHEDULE_SCHEMA]),
vol.Optional(CONF_ZONE_ID): cv.string,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_ENABLED): cv.boolean,
vol.Optional(CONF_MINIMUM): cv.positive_time_period,
vol.Optional(CONF_MAXIMUM): cv.positive_time_period,
vol.Optional(CONF_SHOW): vol.All(SHOW_SCHEMA),
}
)

ALL_ZONES_SCHEMA = vol.Schema(
{
vol.Optional(CONF_SHOW): vol.All(SHOW_SCHEMA),
}
)

SEQUENCE_SCHEDULE_SCHEMA = vol.Schema(
{
vol.Required(CONF_TIME): time_event,
vol.Optional(CONF_DURATION): cv.positive_time_period,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_WEEKDAY): cv.weekdays,
vol.Optional(CONF_MONTH): month_event,
vol.Optional(CONF_DAY): day_event,
}
)

SEQUENCE_ZONE_SCHEMA = vol.Schema(
{
vol.Required(CONF_ZONE_ID): cv.positive_int,
vol.Required(CONF_DURATION): cv.positive_time_period,
}
)
SEQUENCE_SCHEMA = vol.Schema(
{
vol.Required(CONF_SCHEDULES, default={}): vol.All(
cv.ensure_list, [SEQUENCE_SCHEDULE_SCHEMA], _list_is_not_empty
),
vol.Required(CONF_ZONES, default={}): vol.All(
cv.ensure_list, [SEQUENCE_ZONE_SCHEMA], _list_is_not_empty
),
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_DELAY): cv.positive_time_period,
}
)

Expand All @@ -104,11 +156,15 @@ def _list_is_not_empty(value):
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_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_PREAMBLE): cv.positive_time_period,
vol.Optional(CONF_POSTAMBLE): cv.positive_time_period,
vol.Optional(CONF_ENABLED): cv.boolean,
vol.Optional(CONF_ALL_ZONES_CONFIG): vol.All(ALL_ZONES_SCHEMA),
}
)

Expand Down
35 changes: 20 additions & 15 deletions custom_components/irrigation_unlimited/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import homeassistant.util.dt as dt
from datetime import datetime, timedelta
from homeassistant.components import history
from custom_components.irrigation_unlimited.irrigation_unlimited import IUSchedule, IUZone
import json

from homeassistant.const import (
Expand Down Expand Up @@ -98,7 +99,7 @@ class IUMasterEntity(IUEntity):
@property
def unique_id(self):
"""Return a unique ID."""
return f"c{self._controller.controller_index + 1}_m"
return f"c{self._controller.index + 1}_m"

@property
def name(self):
Expand Down Expand Up @@ -130,14 +131,15 @@ def icon(self):
def device_state_attributes(self):
"""Return the state attributes of the device."""
attr = {}
attr["index"] = self._controller.controller_index
attr["index"] = self._controller.index
attr["enabled"] = self._controller.enabled
attr["zone_count"] = len(self._controller._zones)
attr["zones"] = ""
current = self._controller.runs.current_run
if current is not None:
attr["current_zone"] = current.index + 1
attr["current_name"] = current.zone.name
if isinstance(current.parent, IUZone):
attr["current_zone"] = current.parent.index + 1
attr["current_name"] = current.parent.name
attr["current_start"] = dt.as_local(current.start_time)
attr["current_duration"] = str(current.duration)
attr["time_remaining"] = str(current.time_remaining)
Expand All @@ -148,8 +150,9 @@ def device_state_attributes(self):

next = self._controller.runs.next_run
if next is not None:
attr["next_zone"] = next.index + 1
attr["next_name"] = next.zone.name
if isinstance(next.parent, IUZone):
attr["next_zone"] = next.parent.index + 1
attr["next_name"] = next.parent.name
attr["next_start"] = dt.as_local(next.start_time)
attr["next_duration"] = str(next.duration)
else:
Expand All @@ -163,7 +166,7 @@ class IUZoneEntity(IUEntity):
@property
def unique_id(self):
"""Return a unique ID."""
return f"c{self._zone.controller_index + 1}_z{self._zone.zone_index + 1}"
return f"c{self._controller.index + 1}_z{self._zone.index + 1}"

@property
def name(self):
Expand Down Expand Up @@ -198,17 +201,18 @@ def icon(self):
def device_state_attributes(self):
"""Return the state attributes of the device."""
attr = {}
attr["index"] = self._zone.zone_index
attr["zone_id"] = self._zone.zone_id
attr["index"] = self._zone.index
attr["enabled"] = self._zone.enabled and self._controller.enabled
attr["status"] = self._zone.status
attr["schedule_count"] = len(self._zone.schedules)
attr["schedules"] = ""
attr["adjustment"] = self._zone.adjustment.as_string
current = self._zone.runs.current_run
if current is not None:
if current.schedule is not None:
attr["current_schedule"] = current.schedule.schedule_index + 1
attr["current_name"] = current.schedule.name
if isinstance(current.parent, IUSchedule):
attr["current_schedule"] = current.parent.index + 1
attr["current_name"] = current.parent.name
else:
attr["current_schedule"] = RES_MANUAL
attr["current_name"] = RES_MANUAL
Expand All @@ -222,9 +226,9 @@ def device_state_attributes(self):

next = self._zone.runs.next_run
if next is not None:
if next.schedule is not None:
attr["next_schedule"] = next.schedule.schedule_index + 1
attr["next_name"] = next.schedule.name
if isinstance(next.parent, IUSchedule):
attr["next_schedule"] = next.parent.index + 1
attr["next_name"] = next.parent.name
else:
attr["next_schedule"] = RES_MANUAL
attr["next_name"] = RES_MANUAL
Expand All @@ -237,5 +241,6 @@ def device_state_attributes(self):
)
if self._zone.show_config:
attr["configuration"] = json.dumps(self._zone.as_dict(), default=str)
attr["timeline"] = json.dumps(self._zone.runs.as_list(), default=str)
if self._zone.show_timeline:
attr["timeline"] = json.dumps(self._zone.runs.as_list(), default=str)
return attr
6 changes: 6 additions & 0 deletions custom_components/irrigation_unlimited/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@
CONF_DAY = "day"
CONF_ODD = "odd"
CONF_EVEN = "even"
CONF_SHOW = "show"
CONF_CONFIG = "config"
CONF_TIMELINE = "timeline"
CONF_ZONE_ID = "zone_id"
CONF_SEQUENCES = "sequences"
CONF_ALL_ZONES_CONFIG = "all_zones_config"

# Defaults
DEFAULT_NAME = DOMAIN
Expand Down
4 changes: 2 additions & 2 deletions custom_components/irrigation_unlimited/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ def __init__(
self._controller = controller
self._zone = zone # This will be None if it belongs to a Master/Controller
self.entity_id = (
f"{BINARY_SENSOR}.{DOMAIN}_c{self._controller.controller_index + 1}"
f"{BINARY_SENSOR}.{DOMAIN}_c{self._controller.index + 1}"
)
if self._zone is None:
self.entity_id = self.entity_id + "_m"
else:
self.entity_id = self.entity_id + f"_z{self._zone.zone_index + 1}"
self.entity_id = self.entity_id + f"_z{self._zone.index + 1}"
return

async def async_added_to_hass(self):
Expand Down
Loading

0 comments on commit 9fe619c

Please sign in to comment.