Skip to content

Commit

Permalink
Merge commit '1391acb78377ef964f81b162b96ae31b861982b0'
Browse files Browse the repository at this point in the history
  • Loading branch information
kcofoni committed Nov 16, 2023
1 parent 1ca23c7 commit b55090c
Show file tree
Hide file tree
Showing 11 changed files with 248 additions and 25 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
updatefromdev
**/__pycache*
.venv
.venv
pipfile
pipfile.lock
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,24 +41,24 @@ Netro products *Sprite*, *Spark*, *Pixie* and *Whisperer* are actually supported

Please repeat step 4. as mentioned above for each device you want to include, whatever it is a ground sensor, a multi-zone controller (Sprite or Spark) or a single-zone controler (Pixie). Each zone of a controller will be represented by separate device related to the controller it depends on.

![add a config entry](images/add_config_entry.png "Setup of a *Netro* device")
![device is created](images/device_created.png "Sucess of a *Netro* device setup")
![add a config entry](https://kcofoni.github.io/ha-netro-watering/images/add_config_entry.png "Setup of a *Netro* device")
![device is created](https://kcofoni.github.io/ha-netro-watering/images/device_created.png "Sucess of a *Netro* device setup")

At this point, several devices may have been created related to ten's of entity. This latter are representing the humidity, temperature, illuminance of the sensors as well as the current/last/next status of each zone. Switches have been created allowing to start/stop watering and enable/disable controllers.

Options may be changed related to polling refresh interval of sensors and controllers independently. Default watering duration and schedules options may also be changed specifically for the controllers.

![change controller options](images/controller_options.png "Controller options")
![change sensor options](images/sensor_options.png "Sensor options")
![change controller options](https://kcofoni.github.io/ha-netro-watering/images/controller_options.png "Controller options")
![change sensor options](https://kcofoni.github.io/ha-netro-watering/images/sensor_options.png "Sensor options")

**IMPORTANT: to be effective, each time options have been changed, the related device must be reloaded.**

## Lovelace cards
Here are some lovelace cards I am presently using to control my watering system with the help of this integration.

![watering](images/watering-controller-main.png "Controller") ![planning](images/planning-arrosage.png "Planning")
![sensors](images/ground-sensors.png "Sensors") ![start](images/start-watering.png "Start")
![charts](images/courbes-capteurs.png "Charts") ![weather](images/meteo.png "Weather")
![watering](https://kcofoni.github.io/ha-netro-watering/images/watering-controller-main.png "Controller") ![planning](https://kcofoni.github.io/ha-netro-watering/images/planning-arrosage.png "Planning")
![sensors](https://kcofoni.github.io/ha-netro-watering/images/ground-sensors.png "Sensors") ![start](https://kcofoni.github.io/ha-netro-watering/images/start-watering.png "Start")
![charts](https://kcofoni.github.io/ha-netro-watering/images/courbes-capteurs.png "Charts") ![weather](https://kcofoni.github.io/ha-netro-watering/images/meteo.png "Weather")


### Automation
Expand All @@ -68,14 +68,14 @@ The Netro Watering entities may be integrated into automations. The following in
- **Refresh data** - allows to update the data of the devices (controller, zones, sensors) when desired
- **Report weather** - for reporting weather data, overriding system default weather data

![call service](images/service_call.png "Developer Tools")
![call service](https://kcofoni.github.io/ha-netro-watering/images/service_call.png "Developer Tools")

### Set moisture level
The nominal functioning of the Netro ecosystem is based on irrigation planning algorithms that take into account the physiognomy of the areas to be irrigated, the plants that compose them and the properties of the soil, the weather forecast, as well as a certain number of other factors. In addition to this information, Netro needs to know at a given time the temperature and humidity of the areas to be watered in order to precisely determine the watering periods. Soil sensors supplied by Netro (Whisperer model) allow these measurements to be made. If you do not have these sensors which are an integral part of the ecosystem but other external sensors, you can provide Netro with the level of humidity given by these sensors so that it can apply its algorithms in the same way.

The **Set moisture** service provided by the integration and applicable to a particular zone, allows this to be done.

![set moistures](images/set_moisture.png "Developer Tools")
![set moistures](https://kcofoni.github.io/ha-netro-watering/images/set_moisture.png "Developer Tools")

### Report weather
Netro offers to obtain weather data, very useful for establishing automatic watering schedules, from a number of weather providers. In some cases, national services may be more relevant and more precise so that we will want to feed Netro with data from these services instead of the listed providers. The **Report weather** service is offered for this purpose. Each user will be able to establish his own Home Assistant script which will call on this service after having collected custom weather information.
Expand Down
2 changes: 1 addition & 1 deletion custom_components/netro_watering/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
# Here is the list of the platforms that we want to support.
# sensor is for the netro ground sensors, switch is for the zones
# PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.SWITCH]
PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.SWITCH, Platform.BINARY_SENSOR]
PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.SWITCH, Platform.BINARY_SENSOR, Platform.CALENDAR]


_LOGGER = logging.getLogger(__name__)
Expand Down
4 changes: 2 additions & 2 deletions custom_components/netro_watering/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def extra_state_attributes(self) -> dict[str, Any]:
else None,
"last active date": dt_util.as_local(
self.coordinator.metadata.last_active_date.replace(
tzinfo=datetime.timezone.utc
tzinfo=datetime.UTC
)
)
if self.coordinator.metadata is not None
Expand All @@ -143,7 +143,7 @@ def extra_state_attributes(self) -> dict[str, Any]:
else None,
"token reset": dt_util.as_local(
self.coordinator.metadata.token_reset_date.replace(
tzinfo=datetime.timezone.utc
tzinfo=datetime.UTC
)
)
if self.coordinator.metadata is not None
Expand Down
95 changes: 95 additions & 0 deletions custom_components/netro_watering/calendar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""Support for Netro Watering system."""
from __future__ import annotations

import datetime
import logging

from homeassistant.components.calendar import CalendarEntity, CalendarEvent
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import CONF_DEVICE_TYPE, CONTROLLER_DEVICE_TYPE, DOMAIN
from .coordinator import NetroControllerUpdateCoordinator

_LOGGER = logging.getLogger(__name__)

NETRO_CALENDAR_DESCRIPTION = EntityDescription(
key="schedules",
name="Schedules",
entity_registry_enabled_default=True,
translation_key="schedules",
icon="mdi:calendar-clock",
)


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up entry for the controller calendar."""
if config_entry.data[CONF_DEVICE_TYPE] == CONTROLLER_DEVICE_TYPE:
_LOGGER.info("Adding calendar entity")
# add controller calendar
async_add_entities(
[
NetroCalendar(
hass.data[DOMAIN][config_entry.entry_id],
NETRO_CALENDAR_DESCRIPTION,
)
]
)


class NetroCalendar(
CoordinatorEntity[NetroControllerUpdateCoordinator], CalendarEntity
):
"""A calendar implementation for Netro Controller device."""

_attr_has_entity_name = True

def __init__(
self,
coordinator: NetroControllerUpdateCoordinator,
description: EntityDescription,
) -> None:
"""Initialize the Netro calendar."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.serial_number}-{description.key}"
self._attr_device_info = coordinator.device_info

@property
def event(self) -> CalendarEvent | None:
"""Return current or next upcoming schedule if any."""
schedule = self.coordinator.current_calendar_schedule
return (
CalendarEvent(
schedule["start"],
schedule["end"],
schedule["summary"],
schedule["description"],
)
if schedule is not None
else None
)

async def async_get_events(
self,
hass: HomeAssistant,
start_date: datetime.datetime,
end_date: datetime.datetime,
) -> list[CalendarEvent]:
"""Get all schedules in a specific time frame."""
return [
CalendarEvent(
schedule["start"],
schedule["end"],
schedule["summary"],
schedule["description"],
)
for schedule in self.coordinator.calendar_schedules(start_date, end_date)
]
110 changes: 108 additions & 2 deletions custom_components/netro_watering/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from datetime import timedelta
import logging
from time import gmtime, strftime
from typing import Optional

from dateutil.relativedelta import relativedelta

Expand Down Expand Up @@ -435,7 +436,9 @@ def device_info(self) -> DeviceInfo:

# _schedules and _moistures are list of dict whose key = str and value = any
# _active_zones is a dictionary indexed by the zone ith and whose value is a Zone object
# _coming_schedules_ordered is the coming schedules oredered as generated from _schedules
_schedules = []
_coming_schedules_ordered = []
_moistures = []

def __init__(
Expand Down Expand Up @@ -495,6 +498,26 @@ def _update_from_schedules(
Each list is ordered so that the first element return the most recent past schedule and coming schedule respectively.
"""
self._schedules = schedules

# filtering current and coming schedules
coming_schedules_filtered = [
schedule
for schedule in schedules
if schedule[NETRO_SCHEDULE_STATUS]
in [NETRO_SCHEDULE_VALID, NETRO_SCHEDULE_EXECUTING]
and schedule[NETRO_SCHEDULE_START_TIME]
> strftime("%Y-%m-%dT%H:%M:%S", gmtime())
]

# sorting filtered coming schedules on start time ascending
coming_schedules_sorted = sorted(
coming_schedules_filtered,
key=(lambda schedule: schedule[NETRO_SCHEDULE_START_TIME]),
reverse=False,
)
# set the current and coming schedules attribute with the result
self._coming_schedules_ordered = coming_schedules_sorted

for zone_key in self._active_zones:
# filtering past schedules, keeping the current zone
past_schedules_zone_filtered = [
Expand Down Expand Up @@ -587,6 +610,85 @@ def token_remaining(self) -> int | None:
"""Return the remaining token of the controller."""
return self.metadata.token_remaining if self.metadata is not None else None

def calendar_schedules(
self,
start_date: Optional[datetime.date] = None,
end_date: Optional[datetime.date] = None,
):
"""Return the calendar events of the controller."""

return [
{
"start": datetime.datetime.fromisoformat(
schedule[NETRO_SCHEDULE_START_TIME] + TZ_OFFSET
),
"end": datetime.datetime.fromisoformat(
schedule[NETRO_SCHEDULE_END_TIME] + TZ_OFFSET
),
"summary": f"{self._active_zones[schedule[NETRO_SCHEDULE_ZONE]].name}",
"description": "Duration: {} min. - Source: {}".format(
round(
(
datetime.datetime.fromisoformat(
schedule[NETRO_SCHEDULE_END_TIME] + TZ_OFFSET
)
- datetime.datetime.fromisoformat(
schedule[NETRO_SCHEDULE_START_TIME] + TZ_OFFSET
)
).seconds
/ 60
),
schedule[NETRO_SCHEDULE_SOURCE],
),
}
for schedule in self._coming_schedules_ordered
if (
datetime.datetime.fromisoformat(
schedule[NETRO_SCHEDULE_END_TIME] + TZ_OFFSET
)
> start_date
if start_date is not None
else True
)
and (
datetime.datetime.fromisoformat(
schedule[NETRO_SCHEDULE_START_TIME] + TZ_OFFSET
)
< end_date
if end_date is not None
else True
)
]

@property
def current_calendar_schedule(self) -> dict | None:
"""Return current or next coming schedule if any."""
if len(self._coming_schedules_ordered) > 0:
schedule = self._coming_schedules_ordered[0]
return {
"start": datetime.datetime.fromisoformat(
schedule[NETRO_SCHEDULE_START_TIME] + TZ_OFFSET
),
"end": datetime.datetime.fromisoformat(
schedule[NETRO_SCHEDULE_END_TIME] + TZ_OFFSET
),
"summary": f"{self._active_zones[schedule[NETRO_SCHEDULE_ZONE]].name}",
"description": "Duration: {} - Source: {}".format(
round(
(
datetime.datetime.fromisoformat(
schedule[NETRO_SCHEDULE_END_TIME] + TZ_OFFSET
)
- datetime.datetime.fromisoformat(
schedule[NETRO_SCHEDULE_START_TIME] + TZ_OFFSET
)
).seconds
/ 60
),
schedule[NETRO_SCHEDULE_SOURCE],
),
}

async def _async_update_data(self):
"""Fetch data from API endpoint.
Expand Down Expand Up @@ -649,6 +751,7 @@ async def _async_update_data(self):
- next watering event source (smart, fix, manual) - sensor - zone
- battery - sensor - controller (only for non standalone controllers (e.g. Pixie))
- on/off - switch - controller
- schedules - calendar - controller
services
- start watering (controller)
Expand Down Expand Up @@ -708,8 +811,11 @@ async def _async_update_data(self):
zone[NETRO_ZONE_ENABLED],
zone[NETRO_ZONE_SMART],
zone[NETRO_ZONE_NAME]
if zone[NETRO_ZONE_NAME] is not None
else self.device_name + "-" + zone[NETRO_ZONE_ITH],
if (
zone[NETRO_ZONE_NAME] is not None
and len(zone[NETRO_ZONE_NAME]) > 0
)
else self.device_name + "-" + str(zone[NETRO_ZONE_ITH]),
self.serial_number,
)

Expand Down
12 changes: 6 additions & 6 deletions custom_components/netro_watering/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ def extra_state_attributes(self) -> dict[str, Any]:
else None,
"last active date": dt_util.as_local(
self.coordinator.metadata.last_active_date.replace(
tzinfo=datetime.timezone.utc
tzinfo=datetime.UTC
)
)
if self.coordinator.metadata is not None
Expand All @@ -368,7 +368,7 @@ def extra_state_attributes(self) -> dict[str, Any]:
else None,
"token reset": dt_util.as_local(
self.coordinator.metadata.token_reset_date.replace(
tzinfo=datetime.timezone.utc
tzinfo=datetime.UTC
)
)
if self.coordinator.metadata is not None
Expand Down Expand Up @@ -427,7 +427,7 @@ def extra_state_attributes(self) -> dict[str, Any]:
else None,
"last active date": dt_util.as_local(
self.coordinator.metadata.last_active_date.replace(
tzinfo=datetime.timezone.utc
tzinfo=datetime.UTC
)
)
if self.coordinator.metadata is not None
Expand All @@ -443,7 +443,7 @@ def extra_state_attributes(self) -> dict[str, Any]:
else None,
"token reset": dt_util.as_local(
self.coordinator.metadata.token_reset_date.replace(
tzinfo=datetime.timezone.utc
tzinfo=datetime.UTC
)
)
if self.coordinator.metadata is not None
Expand Down Expand Up @@ -512,7 +512,7 @@ def extra_state_attributes(self) -> dict[str, Any]:
else None,
"last active date": dt_util.as_local(
self.coordinator.metadata.last_active_date.replace(
tzinfo=datetime.timezone.utc
tzinfo=datetime.UTC
)
)
if self.coordinator.metadata is not None
Expand All @@ -528,7 +528,7 @@ def extra_state_attributes(self) -> dict[str, Any]:
else None,
"token reset": dt_util.as_local(
self.coordinator.metadata.token_reset_date.replace(
tzinfo=datetime.timezone.utc
tzinfo=datetime.UTC
)
)
if self.coordinator.metadata is not None
Expand Down
Loading

0 comments on commit b55090c

Please sign in to comment.