Skip to content

Commit

Permalink
new version 1.1.0 : custom services, extra states, configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
kcofoni committed Jun 7, 2023
1 parent a93b3fe commit 13b6079
Show file tree
Hide file tree
Showing 15 changed files with 481 additions and 36 deletions.
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,18 +61,23 @@ No dedicated card has been implemented yet but perhaps there will be user contri

### Automation
The Netro Watering entities may be integrated into automations. The following integration custom services are available:
- **start watering** and **stop watering** services - to be applied to any controller or zone.
- **enable** and **disable** services - to be applied to any controller.
- **Start watering** and **Stop watering** services - to be applied to any controller or zone.
- **Enable** and **Disable** services - to be applied to any controller.
- **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")

### 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.
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")

### 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.

## Advanced configuration
Some general settings can be set for the Netro Watering integration in the Home Assistant configuration file (*configuration.yaml*). They correspond to both optional and non-device specific parameters. The integration works well without its parameters which can nevertheless provide optimizations and respond to specific situations. If not set, the default values are applied.

Expand Down
233 changes: 228 additions & 5 deletions custom_components/netro_watering/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Support for Netro Watering system."""
from __future__ import annotations

from datetime import date
import enum
import logging

import validators
Expand All @@ -12,9 +14,22 @@
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator

from .const import (
ATTR_CONFIG_ENTRY_ID,
ATTR_MOISTURE,
ATTR_WEATHER_CONDITION,
ATTR_WEATHER_DATE,
ATTR_WEATHER_HUMIDITY,
ATTR_WEATHER_PRESSURE,
ATTR_WEATHER_RAIN,
ATTR_WEATHER_RAIN_PROB,
ATTR_WEATHER_T_DEW,
ATTR_WEATHER_T_MAX,
ATTR_WEATHER_T_MIN,
ATTR_WEATHER_TEMP,
ATTR_WEATHER_WIND_SPEED,
ATTR_ZONE_ID,
CONF_CTRL_REFRESH_INTERVAL,
CONF_DEVICE_HW_VERSION,
Expand All @@ -34,6 +49,7 @@
GLOBAL_PARAMETERS,
MONTHS_AFTER_SCHEDULES,
MONTHS_BEFORE_SCHEDULES,
NETRO_DEFAULT_ZONE_MODEL,
SENS_REFRESH_INTERVAL_MN,
SENSOR_DEVICE_TYPE,
)
Expand All @@ -42,7 +58,11 @@
NetroSensorUpdateCoordinator,
prepare_slowdown_factors,
)
from .netrofunction import set_moisture as netro_set_moisture, set_netro_base_url
from .netrofunction import (
report_weather as netro_report_weather,
set_moisture as netro_set_moisture,
set_netro_base_url,
)

# Here is the list of the platforms that we want to support.
# sensor is for the netro ground sensors, switch is for the zones
Expand All @@ -64,6 +84,58 @@
}
)

SERVICE_REFRESH_NAME = "refresh_data"
SERVICE_REFRESH_SCHEMA = vol.Schema(
{
vol.Required(ATTR_CONFIG_ENTRY_ID): cv.string,
}
)


class WeatherConditions(enum.Enum):
"""Class to represent the possible weather conditions."""

clear = 0
cloudy = 1
rain = 2
snow = 3
wind = 4


SERVICE_REPORT_WEATHER_NAME = "report_weather"
SERVICE_REPORT_WEATHER_SCHEMA = vol.Schema(
{
vol.Required(ATTR_CONFIG_ENTRY_ID): cv.string,
vol.Required(ATTR_WEATHER_DATE): cv.date,
vol.Optional(ATTR_WEATHER_CONDITION): cv.enum(WeatherConditions),
vol.Optional(ATTR_WEATHER_RAIN): cv.positive_float,
vol.Optional(ATTR_WEATHER_RAIN_PROB): vol.All(
vol.Coerce(int), vol.Range(min=0, max=100)
),
vol.Optional(ATTR_WEATHER_TEMP): vol.All(
vol.Coerce(float), vol.Range(min=0, max=60)
),
vol.Optional(ATTR_WEATHER_T_MIN): vol.All(
vol.Coerce(float), vol.Range(min=0, max=60)
),
vol.Optional(ATTR_WEATHER_T_MAX): vol.All(
vol.Coerce(float), vol.Range(min=0, max=60)
),
vol.Optional(ATTR_WEATHER_T_DEW): vol.All(
vol.Coerce(float), vol.Range(min=0, max=60)
),
vol.Optional(ATTR_WEATHER_WIND_SPEED): vol.All(
vol.Coerce(float), vol.Range(min=0, max=60)
),
vol.Required(ATTR_WEATHER_HUMIDITY): vol.All(
vol.Coerce(int), vol.Range(min=0, max=100)
),
vol.Optional(ATTR_WEATHER_PRESSURE): vol.All(
vol.Coerce(float), vol.Range(min=850.0, max=1100.0)
),
}
)


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Init of the integration."""
Expand Down Expand Up @@ -202,7 +274,10 @@ async def set_moisture(call: ServiceCall) -> None:
raise HomeAssistantError(
f"Invalid Netro Watering device ID: {device_id}"
)
if device_entry.model is None or "zone" not in device_entry.model.lower():
if (
device_entry.model is None
or device_entry.model != NETRO_DEFAULT_ZONE_MODEL
):
raise HomeAssistantError(
f"Invalid Netro Watering device ID: {device_id}, it doesn't seem to be a zone !?"
)
Expand All @@ -229,7 +304,7 @@ async def set_moisture(call: ServiceCall) -> None:

# set moisture by Netro
_LOGGER.info(
"Running custom service set_moisture : the humidity level has been forced to %s%% for zone %s (id = %s)",
"Running custom service 'Set moisture' : the humidity level has been forced to %s%% for zone %s (id = %s)",
moisture,
device_entry.name,
zone_id,
Expand All @@ -238,7 +313,7 @@ async def set_moisture(call: ServiceCall) -> None:
netro_set_moisture, key, moisture, zone_id
)

# only one moisture_service to be created for all controllers
# only one Set moisture service to be created for all controllers
if not hass.services.has_service(DOMAIN, SERVICE_SET_MOISTURE_NAME):
_LOGGER.info("Adding custom service : %s", SERVICE_SET_MOISTURE_NAME)
hass.services.async_register(
Expand All @@ -248,6 +323,141 @@ async def set_moisture(call: ServiceCall) -> None:
schema=SERVICE_SET_MOISTURE_SCHEMA,
)

async def report_weather(call: ServiceCall) -> None:
weather_asof: date = call.data[ATTR_WEATHER_DATE]
weather_condition = (
call.data[ATTR_WEATHER_CONDITION]
if call.data.get(ATTR_WEATHER_CONDITION) is not None
else None
)
weather_rain = (
call.data[ATTR_WEATHER_RAIN]
if call.data.get(ATTR_WEATHER_RAIN) is not None
else None
)
weather_rain_prob = (
call.data[ATTR_WEATHER_RAIN_PROB]
if call.data.get(ATTR_WEATHER_RAIN_PROB) is not None
else None
)
weather_temp = (
call.data[ATTR_WEATHER_TEMP]
if call.data.get(ATTR_WEATHER_TEMP) is not None
else None
)
weather_t_min = (
call.data[ATTR_WEATHER_T_MIN]
if call.data.get(ATTR_WEATHER_T_MIN) is not None
else None
)
weather_t_max = (
call.data[ATTR_WEATHER_T_MAX]
if call.data.get(ATTR_WEATHER_T_MAX) is not None
else None
)
weather_t_dew = (
call.data[ATTR_WEATHER_T_DEW]
if call.data.get(ATTR_WEATHER_T_DEW) is not None
else None
)
weather_wind_speed = (
call.data[ATTR_WEATHER_WIND_SPEED]
if call.data.get(ATTR_WEATHER_WIND_SPEED) is not None
else None
)
weather_humidity = (
call.data[ATTR_WEATHER_HUMIDITY]
if call.data.get(ATTR_WEATHER_HUMIDITY) is not None
else None
)
weather_pressure = (
call.data[ATTR_WEATHER_PRESSURE]
if call.data.get(ATTR_WEATHER_PRESSURE) is not None
else None
)

# get serial number
entry_id = call.data[ATTR_CONFIG_ENTRY_ID]
if entry_id not in hass.data[DOMAIN]:
raise HomeAssistantError(f"Config entry id does not exist: {entry_id}")
coordinator = hass.data[DOMAIN][entry_id]

key = coordinator.serial_number

# report weather by Netro
_LOGGER.info(
"Running custom service report_weather : %s",
{
"controller": coordinator.name,
"date": str(weather_asof) if weather_asof else weather_asof,
"condition": weather_condition.value,
"rain": weather_rain,
"rain_prob": weather_rain_prob,
"temp": weather_temp,
"t_min": weather_t_min,
"t_max": weather_t_max,
"t_dew": weather_t_dew,
"wind_speed": weather_wind_speed,
"humidity": int(weather_humidity),
"pressure": weather_pressure,
},
)

if not weather_asof:
raise HomeAssistantError(
"'date' parameter is missing when running 'Report weather' service provided by Netro Watering integration"
)

await hass.async_add_executor_job(
netro_report_weather,
key,
str(weather_asof),
weather_condition.value,
weather_rain,
weather_rain_prob,
weather_temp,
weather_t_min,
weather_t_max,
weather_t_dew,
weather_wind_speed,
weather_humidity,
weather_pressure,
)

# only one Report weather service to be created for all config entries
if not hass.services.has_service(DOMAIN, SERVICE_REPORT_WEATHER_NAME):
_LOGGER.info("Adding custom service : %s", SERVICE_REPORT_WEATHER_NAME)
hass.services.async_register(
DOMAIN,
SERVICE_REPORT_WEATHER_NAME,
report_weather,
schema=SERVICE_REPORT_WEATHER_SCHEMA,
)

async def refresh(call: ServiceCall) -> None:
"""Service call to refresh data of Netro devices."""

entry_id = call.data[ATTR_CONFIG_ENTRY_ID]
if entry_id not in hass.data[DOMAIN]:
raise HomeAssistantError(f"Config entry id does not exist: {entry_id}")
coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry_id]

_LOGGER.info(
"Running custom service 'Refresh data' for %s devices", coordinator.name
)

await coordinator.async_request_refresh()

# only one Refresh data service to be created for all config entry
if not hass.services.has_service(DOMAIN, SERVICE_REFRESH_NAME):
_LOGGER.info("Adding custom service : %s", SERVICE_REFRESH_NAME)
hass.services.async_register(
DOMAIN,
SERVICE_REFRESH_NAME,
refresh,
schema=SERVICE_REFRESH_SCHEMA,
)

return True


Expand All @@ -257,7 +467,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.info("Deleting %s", hass.data[DOMAIN][entry.entry_id])
hass.data[DOMAIN].pop(entry.entry_id)

# the set_moisture service has to be removed if the current entry is a controller and the last one
# the Set moisture service has to be removed if the current entry is a controller and the last one
if entry.data[CONF_DEVICE_TYPE] == CONTROLLER_DEVICE_TYPE:
loaded_entries = [
entry
Expand All @@ -270,4 +480,17 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.info("Removing service %s", SERVICE_SET_MOISTURE_NAME)
hass.services.async_remove(DOMAIN, SERVICE_SET_MOISTURE_NAME)

# if there is no more entry after this one, one must remove the config entry level services
loaded_entries = [
entry
for entry in hass.config_entries.async_entries(DOMAIN)
if entry.state == ConfigEntryState.LOADED
]

if len(loaded_entries) == 1:
_LOGGER.info("Removing service %s", SERVICE_REPORT_WEATHER_NAME)
hass.services.async_remove(DOMAIN, SERVICE_REPORT_WEATHER_NAME)
_LOGGER.info("Removing service %s", SERVICE_REFRESH_NAME)
hass.services.async_remove(DOMAIN, SERVICE_REFRESH_NAME)

return unload_ok
Loading

0 comments on commit 13b6079

Please sign in to comment.