Skip to content

Commit

Permalink
Volume tracking
Browse files Browse the repository at this point in the history
  • Loading branch information
rgc99 committed Oct 1, 2023
1 parent 7105091 commit eaad8bc
Show file tree
Hide file tree
Showing 8 changed files with 1,691 additions and 0 deletions.
1 change: 1 addition & 0 deletions custom_components/irrigation_unlimited/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,4 +257,5 @@ def extra_state_attributes(self):
attr[ATTR_CONFIGURATION] = self._zone.configuration
if self._zone.show_timeline:
attr[ATTR_TIMELINE] = self._zone.timeline()
attr["volume"] = self._zone.volume.total
return attr
2 changes: 2 additions & 0 deletions custom_components/irrigation_unlimited/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@
CONF_STATE_OFF = "state_off"
CONF_SCHEDULE_ID = "schedule_id"
CONF_FROM = "from"
CONF_VOLUME = "volume"
CONF_PRECISION = "precision"

# Defaults
DEFAULT_NAME = DOMAIN
Expand Down
108 changes: 108 additions & 0 deletions custom_components/irrigation_unlimited/irrigation_unlimited.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from homeassistant.helpers.event import (
async_track_point_in_utc_time,
async_call_later,
async_track_state_change_event,
)
from homeassistant.helpers import sun
from homeassistant.util import dt
Expand Down Expand Up @@ -192,6 +193,8 @@
CONF_STATE_OFF,
CONF_SCHEDULE_ID,
CONF_FROM,
CONF_VOLUME,
CONF_PRECISION,
)

_LOGGER: Logger = getLogger(__package__)
Expand Down Expand Up @@ -1473,6 +1476,92 @@ def call_switch(self, state: bool, stime: datetime = None) -> None:
self._check_back_time = stime + self._check_back_delay


class IUVolume:
"""Irrigation Unlimited Volume class"""

def __init__(self, hass: HomeAssistant, coordinator: "IUCoordinator") -> None:
# Passed parameters
self._hass = hass
self._coordinator = coordinator
# Config parameters
self._sensor_id: str = None
self._rounding = 3
# Private variables
self._callback_remove: CALLBACK_TYPE = None
self._start: float = None
self._total: float = None

@property
def total(self) -> str | None:
"""Return the total value"""
if self._total is not None:
return format(self._total, f".{self._rounding}f")
return None

def load(self, config: OrderedDict, all_zones: OrderedDict) -> "IUSwitch":
"""Load volume data from the configuration"""

def load_params(config: OrderedDict) -> None:
if config is None:
return
self._sensor_id = config.get(CONF_ENTITY_ID, self._sensor_id)
self._rounding = config.get(CONF_PRECISION, self._rounding)

if all_zones is not None:
load_params(all_zones.get(CONF_VOLUME))
load_params(config.get(CONF_VOLUME))

def start_record(self, stime: datetime) -> None:
"""Start recording volume information"""
if self._sensor_id is None:
return

def sensor_state_change(event: HAEvent):
# pylint: disable=unused-argument
sensor = self._hass.states.get(self._sensor_id)
if sensor is not None:
try:
value = float(sensor.state)
except ValueError:
return
self._total = value - self._start

self._start = self._total = None
sensor = self._hass.states.get(self._sensor_id)
if sensor is not None:
try:
self._start = float(sensor.state)
except ValueError:
self._coordinator.logger.log_invalid_meter_value(stime, sensor.state)
else:
self._callback_remove = async_track_state_change_event(
self._hass, self._sensor_id, sensor_state_change
)
else:
self._coordinator.logger.log_invalid_meter_id(stime, self._sensor_id)

def end_record(self, stime: datetime) -> None:
"""Finish recording volume information"""
if self._callback_remove is not None:
self._callback_remove()
self._callback_remove = None
if self._start is not None:
sensor = self._hass.states.get(self._sensor_id)
if sensor is not None:
try:
value = float(sensor.state)
except ValueError:
self._coordinator.logger.log_invalid_meter_value(
stime, sensor.state
)
self._total = None
else:
self._total = value - self._start
else:
self._coordinator.logger.log_invalid_meter_id(stime, self._sensor_id)
self._total = None


class IUZone(IUBase):
"""Irrigation Unlimited Zone class"""

Expand Down Expand Up @@ -1512,6 +1601,7 @@ def __init__(
self._suspend_until: datetime = None
self._dirty: bool = True
self._switch = IUSwitch(hass, coordinator, controller, self)
self._volume = IUVolume(hass, coordinator)

@property
def unique_id(self) -> str:
Expand Down Expand Up @@ -1545,6 +1635,11 @@ def adjustment(self) -> IUAdjustment:
"""Return the adjustment for this zone"""
return self._adjustment

@property
def volume(self) -> IUVolume:
"""Return the volume for this zone"""
return self._volume

@property
def zone_id(self) -> str:
"""Return the zone_id. Should match the zone_id used in sequences"""
Expand Down Expand Up @@ -1784,6 +1879,7 @@ def load(self, config: OrderedDict, all_zones: OrderedDict) -> "IUZone":
for sidx, schedule_config in enumerate(config[CONF_SCHEDULES]):
self.find_add(sidx).load(schedule_config)
self._switch.load(config, all_zones)
self._volume.load(config, all_zones)
self._dirty = True
return self

Expand Down Expand Up @@ -3431,6 +3527,7 @@ def check_run(self, stime: datetime) -> bool:
for zone in (self._zones[i] for i in zones_changed):
if not zone.is_on:
zone.call_switch(zone.is_on, stime)
zone.volume.end_record(stime)

# Check if master has changed and update
if state_changed:
Expand All @@ -3442,6 +3539,7 @@ def check_run(self, stime: datetime) -> bool:
for zone in (self._zones[i] for i in zones_changed):
if zone.is_on:
zone.call_switch(zone.is_on, stime)
zone.volume.start_record(stime)

return state_changed

Expand Down Expand Up @@ -4514,6 +4612,16 @@ def log_invalid_crontab(
f"error: {msg}",
)

def log_invalid_meter_id(
self, stime: datetime, entity_id: str, level=ERROR
) -> None:
"""Warn the volume meter is invalid"""
self._format(level, "VOLUME_SENSOR", stime, f"entity_id: {entity_id}")

def log_invalid_meter_value(self, stime: datetime, value: str, level=ERROR) -> None:
"""Warn the volume meter value is invalid"""
self._format(level, "VOLUME_VALUE", stime, f"value: {value}")


class IUClock:
"""Irrigation Unlimited Clock class"""
Expand Down
11 changes: 11 additions & 0 deletions custom_components/irrigation_unlimited/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@
CONF_STATES,
CONF_SCHEDULE_ID,
CONF_FROM,
CONF_VOLUME,
CONF_PRECISION,
)

IU_ID = r"^[a-z0-9]+(_[a-z0-9]+)*$"
Expand Down Expand Up @@ -195,6 +197,13 @@ def _parse_dd_mmm(value: str) -> date | None:
}
)

VOLUME_SCHEMA = vol.Schema(
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_PRECISION): cv.positive_int,
}
)

ZONE_SCHEMA = vol.Schema(
{
vol.Optional(CONF_SCHEDULES): vol.All(cv.ensure_list, [SCHEDULE_SCHEMA]),
Expand All @@ -208,6 +217,7 @@ def _parse_dd_mmm(value: str) -> date | None:
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,
}
)
Expand All @@ -220,6 +230,7 @@ def _parse_dd_mmm(value: str) -> date | None:
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,
}
)
Expand Down
140 changes: 140 additions & 0 deletions tests/configs/test_volume.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
default_config:

# Dummy sensor
input_text:
dummy_sensor_1:
name: Dummy Sensor 1
initial: 123
dummy_sensor_2:
name: Dummy Sensor 2
initial: 567

irrigation_unlimited:
refresh_interval: 2000
controllers:
- name: "Test controller 1"
zones:
- name: "Zone 1"
volume:
entity_id: "input_text.dummy_sensor_1"
- name: "Zone 2"
volume:
entity_id: "input_text.dummy_sensor_2"
- name: "Zone 3"
volume:
entity_id: "input_text.dummy_sensor_3" # Does not exist!
sequences:
- name: "Sequence 1"
delay: "0:01:00"
schedules:
- time: "06:05"
zones:
- zone_id: 1
duration: "0:06:00"
- zone_id: 2
duration: "0:12:00"
- zone_id: 3
duration: "0:18:00"
testing:
enabled: true
speed: 1.0
output_events: false
show_log: false
autoplay: false
times:
- name: "1-Sequence 1"
start: "2021-01-04 06:00"
end: "2021-01-04 07:00"
results:
- {t: '2021-01-04 06:05', c: 1, z: 0, s: 1}
- {t: '2021-01-04 06:05', c: 1, z: 1, s: 1}
- {t: '2021-01-04 06:11', c: 1, z: 1, s: 0}
- {t: '2021-01-04 06:11', c: 1, z: 0, s: 0}
- {t: '2021-01-04 06:12', c: 1, z: 0, s: 1}
- {t: '2021-01-04 06:12', c: 1, z: 2, s: 1}
- {t: '2021-01-04 06:24', c: 1, z: 2, s: 0}
- {t: '2021-01-04 06:24', c: 1, z: 0, s: 0}
- {t: '2021-01-04 06:25', c: 1, z: 0, s: 1}
- {t: '2021-01-04 06:25', c: 1, z: 3, s: 1}
- {t: '2021-01-04 06:43', c: 1, z: 3, s: 0}
- {t: '2021-01-04 06:43', c: 1, z: 0, s: 0}
- name: "2-Sequence 1"
start: "2021-01-04 06:00"
end: "2021-01-04 07:00"
results:
- {t: '2021-01-04 06:05', c: 1, z: 0, s: 1}
- {t: '2021-01-04 06:05', c: 1, z: 1, s: 1}
- {t: '2021-01-04 06:11', c: 1, z: 1, s: 0}
- {t: '2021-01-04 06:11', c: 1, z: 0, s: 0}
- {t: '2021-01-04 06:12', c: 1, z: 0, s: 1}
- {t: '2021-01-04 06:12', c: 1, z: 2, s: 1}
- {t: '2021-01-04 06:24', c: 1, z: 2, s: 0}
- {t: '2021-01-04 06:24', c: 1, z: 0, s: 0}
- {t: '2021-01-04 06:25', c: 1, z: 0, s: 1}
- {t: '2021-01-04 06:25', c: 1, z: 3, s: 1}
- {t: '2021-01-04 06:43', c: 1, z: 3, s: 0}
- {t: '2021-01-04 06:43', c: 1, z: 0, s: 0}
- name: "3-Sequence 1"
start: "2021-01-04 06:00"
end: "2021-01-04 07:00"
results:
- {t: '2021-01-04 06:05', c: 1, z: 0, s: 1}
- {t: '2021-01-04 06:05', c: 1, z: 1, s: 1}
- {t: '2021-01-04 06:11', c: 1, z: 1, s: 0}
- {t: '2021-01-04 06:11', c: 1, z: 0, s: 0}
- {t: '2021-01-04 06:12', c: 1, z: 0, s: 1}
- {t: '2021-01-04 06:12', c: 1, z: 2, s: 1}
- {t: '2021-01-04 06:24', c: 1, z: 2, s: 0}
- {t: '2021-01-04 06:24', c: 1, z: 0, s: 0}
- {t: '2021-01-04 06:25', c: 1, z: 0, s: 1}
- {t: '2021-01-04 06:25', c: 1, z: 3, s: 1}
- {t: '2021-01-04 06:43', c: 1, z: 3, s: 0}
- {t: '2021-01-04 06:43', c: 1, z: 0, s: 0}
- name: "4-Sequence 1"
start: "2021-01-04 06:00"
end: "2021-01-04 07:00"
results:
- {t: '2021-01-04 06:05', c: 1, z: 0, s: 1}
- {t: '2021-01-04 06:05', c: 1, z: 1, s: 1}
- {t: '2021-01-04 06:11', c: 1, z: 1, s: 0}
- {t: '2021-01-04 06:11', c: 1, z: 0, s: 0}
- {t: '2021-01-04 06:12', c: 1, z: 0, s: 1}
- {t: '2021-01-04 06:12', c: 1, z: 2, s: 1}
- {t: '2021-01-04 06:24', c: 1, z: 2, s: 0}
- {t: '2021-01-04 06:24', c: 1, z: 0, s: 0}
- {t: '2021-01-04 06:25', c: 1, z: 0, s: 1}
- {t: '2021-01-04 06:25', c: 1, z: 3, s: 1}
- {t: '2021-01-04 06:43', c: 1, z: 3, s: 0}
- {t: '2021-01-04 06:43', c: 1, z: 0, s: 0}
- name: "5-Sequence 1"
start: "2021-01-04 06:00"
end: "2021-01-04 07:00"
results:
- {t: '2021-01-04 06:05', c: 1, z: 0, s: 1}
- {t: '2021-01-04 06:05', c: 1, z: 1, s: 1}
- {t: '2021-01-04 06:11', c: 1, z: 1, s: 0}
- {t: '2021-01-04 06:11', c: 1, z: 0, s: 0}
- {t: '2021-01-04 06:12', c: 1, z: 0, s: 1}
- {t: '2021-01-04 06:12', c: 1, z: 2, s: 1}
- {t: '2021-01-04 06:24', c: 1, z: 2, s: 0}
- {t: '2021-01-04 06:24', c: 1, z: 0, s: 0}
- {t: '2021-01-04 06:25', c: 1, z: 0, s: 1}
- {t: '2021-01-04 06:25', c: 1, z: 3, s: 1}
- {t: '2021-01-04 06:43', c: 1, z: 3, s: 0}
- {t: '2021-01-04 06:43', c: 1, z: 0, s: 0}
- name: "6-Sequence 1"
start: "2021-01-04 06:00"
end: "2021-01-04 07:00"
results:
- {t: '2021-01-04 06:05', c: 1, z: 0, s: 1}
- {t: '2021-01-04 06:05', c: 1, z: 1, s: 1}
- {t: '2021-01-04 06:11', c: 1, z: 1, s: 0}
- {t: '2021-01-04 06:11', c: 1, z: 0, s: 0}
- {t: '2021-01-04 06:12', c: 1, z: 0, s: 1}
- {t: '2021-01-04 06:12', c: 1, z: 2, s: 1}
- {t: '2021-01-04 06:24', c: 1, z: 2, s: 0}
- {t: '2021-01-04 06:24', c: 1, z: 0, s: 0}
- {t: '2021-01-04 06:25', c: 1, z: 0, s: 1}
- {t: '2021-01-04 06:25', c: 1, z: 3, s: 1}
- {t: '2021-01-04 06:43', c: 1, z: 3, s: 0}
- {t: '2021-01-04 06:43', c: 1, z: 0, s: 0}
Loading

0 comments on commit eaad8bc

Please sign in to comment.