Add Home Assistant custom integration and ESP availability tracking

- Add custom HA integration (config flow, switch, water_for service)
- Add MQTT Last Will & Testament for offline detection
- Publish 'online' on successful MQTT connect
- Update README with integration docs
This commit is contained in:
2026-07-19 15:25:04 +02:00
parent 3bd296c87b
commit 884182e1f7
11 changed files with 680 additions and 13 deletions

View File

@@ -1,15 +1,41 @@
# Irrigation with an ESP8266 # Irrigation with an ESP8266
Tested with D1 mini. Connect a pump with a relais. Relais activation on D4. Tested with D1 mini. Connect a pump with a relais. Relais activation on D4.
## Homeassistant integration ## Home Assistant Integration
payload_on can also contain the duration of the pump in ms.
Example: ### Custom Integration (recommended)
`
switch 5: A full custom integration with config flow, device registry, and a `water_for` service is included in the [`homeassistant/`](homeassistant/) directory.
- platform: mqtt
See [homeassistant/README.md](homeassistant/README.md) for installation and usage.
### YAML-only (built-in MQTT platform)
`payload_on` can also contain the duration of the pump in ms.
```yaml
switch:
- platform: mqtt
name: "Irrigation Pump"
command_topic: greenhousino/pump command_topic: greenhousino/pump
state_topic: greenhousino/pumpstate state_topic: greenhousino/pumpstate
payload_on: "on" payload_on: "on"
payload_off: "off" payload_off: "off"
name: "Pumpe" state_on: "on"
` state_off: "off"
availability_topic: greenhousino/pump/status
payload_available: "online"
payload_not_available: "offline"
qos: 1
```
For duration-based watering via YAML, use an automation:
```yaml
automation:
- alias: "Water for 5 minutes"
action:
- service: mqtt.publish
data:
topic: greenhousino/pump
payload: "300000" # 300000 ms = 5 minutes
```

View File

@@ -0,0 +1,124 @@
# Greenhousino Irrigation — Home Assistant Integration
Custom Home Assistant integration for the Greenhousino ESP8266/ESP32 irrigation controller.
## Features
- **Pump switch** — Turn the irrigation pump on/off from Home Assistant
- **State tracking** — Real-time pump state via MQTT
- **Availability** — Shows online/offline status
- **`water_for` service** — Water for a specific duration (13600 seconds)
- **Config flow** — Set up via Home Assistant UI (Settings → Devices & Services → Add Integration)
- **Device registry** — Appears as a proper device with manufacturer/model info
## Installation
### Option 1: HACS (recommended)
1. Install [HACS](https://hacs.xyz/) in Home Assistant
2. Add this repository as a custom repository
3. Search for "Greenhousino Irrigation" and install
4. Restart Home Assistant
### Option 2: Manual installation
1. Copy the `greenhousino` folder to your Home Assistant config directory:
```bash
cp -r greenhousino /config/custom_components/
```
2. Restart Home Assistant
3. Go to **Settings → Devices & Services → Add Integration** and search for "Greenhousino Irrigation"
## Configuration
The config flow will guide you through two steps:
### Step 1: Host
- **Host IP address** — The IP of your ESP device (e.g., `192.168.178.50`). This is used for the web UI link in device info.
### Step 2: MQTT Topics
- **Command topic** — MQTT topic to send commands (default: `greenhousino/pump`)
- **State topic** — MQTT topic for state updates (default: `greenhousino/pumpstate`)
## Usage
### Switch entity
The integration creates a switch entity (`switch.greenhousino_irrigation_pump`) that you can:
- Toggle on/off from the Home Assistant UI
- Use in automations
- Add to dashboards
### `water_for` service
Water for a specific duration:
```yaml
service: greenhousino.water_for
data:
duration: 60 # seconds
```
Or from an automation:
```yaml
automation:
- alias: "Water garden every morning"
trigger:
- platform: time
at: "06:00:00"
action:
- service: greenhousino.water_for
data:
duration: 300 # 5 minutes
```
## MQTT Topics
| Direction | Topic | Payload | Description |
|-----------|-------|---------|-------------|
| HA → ESP | `greenhousino/pump` | `on` | Turn pump on (default 30s) |
| HA → ESP | `greenhousino/pump` | `off` | Turn pump off |
| HA → ESP | `greenhousino/pump` | `5000` | Turn pump on for 5000ms |
| ESP → HA | `greenhousino/pumpstate` | `on` | Pump is active |
| ESP → HA | `greenhousino/pumpstate` | `off` | Pump is off |
| ESP → HA | `greenhousino/pump/status` | `offline` | Device disconnected |
## Requirements
- Home Assistant 2024.x or later
- MQTT integration configured and connected to the same broker as your ESP device
## Troubleshooting
### Switch shows "unavailable"
- Make sure the MQTT integration is connected
- Verify the ESP device is publishing to the state topic
- Check that topics match between the ESP firmware and HA config
### Service call doesn't work
- Ensure MQTT integration is set up
- Check HA logs: `Logger → greenhousino`
- Verify the ESP is subscribed to the command topic
## YAML-only alternative (no custom component)
If you don't want a custom integration, you can use the built-in MQTT platform:
```yaml
switch:
- platform: mqtt
name: "Irrigation Pump"
command_topic: "greenhousino/pump"
state_topic: "greenhousino/pumpstate"
payload_on: "on"
payload_off: "off"
state_on: "on"
state_off: "off"
availability_topic: "greenhousino/pump/status"
payload_available: "online"
payload_not_available: "offline"
qos: 1
```

View File

@@ -0,0 +1,143 @@
"""Greenhousino Irrigation integration."""
import logging
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from .const import (
CONF_COMMAND_TOPIC,
CONF_HOST,
CONF_STATE_TOPIC,
DEFAULT_COMMAND_TOPIC,
DEFAULT_STATE_TOPIC,
DOMAIN,
MANUFACTURER,
MODEL,
SERVICE_DATA_DURATION,
SERVICE_WATER_FOR,
)
_LOGGER = logging.getLogger(__name__)
PLATFORMS = ["switch"]
# Service schema
WATER_FOR_SCHEMA = vol.Schema(
{
vol.Required(SERVICE_DATA_DURATION): vol.All(
vol.Coerce(float),
vol.Range(min=1, max=3600),
),
}
)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Greenhousino from a config entry."""
hass_data = entry.runtime_data = GreenhousinoData(entry)
host = entry.data.get(CONF_HOST, "")
# Register device in HA device registry
await _register_device(hass, entry, host)
# Store entry data for service access
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {"entries": {}}
hass.data[DOMAIN]["entries"][entry.entry_id] = hass_data
# Register the water_for service (only once)
if "service_registered" not in hass.data[DOMAIN]:
hass.services.async_register(
DOMAIN,
SERVICE_WATER_FOR,
_make_water_for_handler(hass),
schema=WATER_FOR_SCHEMA,
)
hass.data[DOMAIN]["service_registered"] = True
_LOGGER.info("Registered %s.%s service", DOMAIN, SERVICE_WATER_FOR)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN]["entries"].pop(entry.entry_id, None)
if len(hass.data[DOMAIN]["entries"]) == 0:
hass.services.async_remove(DOMAIN, SERVICE_WATER_FOR)
hass.data[DOMAIN].clear()
return unload_ok
def _make_water_for_handler(hass: HomeAssistant):
"""Create the water_for service handler with hass in closure."""
async def async_water_for(call) -> None:
"""Handle the water_for service call."""
duration_seconds = call.data[SERVICE_DATA_DURATION]
duration_ms = int(duration_seconds * 1000)
# Use first available command topic (or default)
entries = hass.data[DOMAIN].get("entries", {})
command_topic = DEFAULT_COMMAND_TOPIC
for entry_id, data in entries.items():
command_topic = data.command_topic
_LOGGER.info(
"Watering for %d seconds (%d ms) on topic %s (entry: %s)",
duration_seconds,
duration_ms,
command_topic,
entry_id,
)
await hass.services.async_call(
"mqtt",
"publish",
{
"topic": command_topic,
"payload": str(duration_ms),
"qos": 1,
"retain": False,
},
blocking=True,
)
if not entries:
_LOGGER.warning("No Greenhousino entries configured")
return async_water_for
async def _register_device(hass: HomeAssistant, entry: ConfigEntry, host: str):
"""Register the device in the device registry."""
dr.async_get(hass).async_get_or_create(
config_entry_id=entry.entry_id,
connections=set(),
identifiers={(DOMAIN, entry.entry_id)},
manufacturer=MANUFACTURER,
model=MODEL,
name=entry.title,
configuration_url=f"http://{host}" if host else None,
)
class GreenhousinoData:
"""Container for integration data."""
def __init__(self, entry: ConfigEntry) -> None:
self.entry = entry
self.command_topic = entry.data.get(
CONF_COMMAND_TOPIC, DEFAULT_COMMAND_TOPIC
)
self.state_topic = entry.data.get(CONF_STATE_TOPIC, DEFAULT_STATE_TOPIC)

View File

@@ -0,0 +1,88 @@
"""Config flow for Greenhousino Irrigation integration."""
import logging
from typing import Any
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.core import HomeAssistant
from .const import (
CONF_COMMAND_TOPIC,
CONF_HOST,
CONF_STATE_TOPIC,
DEFAULT_COMMAND_TOPIC,
DEFAULT_NAME,
DEFAULT_STATE_TOPIC,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
STEP_ONE_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST, description={"suggested_value": ""}): str,
}
)
STEP_TWO_DATA_SCHEMA = vol.Schema(
{
vol.Required(
CONF_COMMAND_TOPIC,
description={"suggested_value": DEFAULT_COMMAND_TOPIC},
): str,
vol.Required(
CONF_STATE_TOPIC,
description={"suggested_value": DEFAULT_STATE_TOPIC},
): str,
}
)
class GreenhousinoConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Greenhousino Irrigation."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step (host)."""
errors: dict[str, str] = {}
if user_input is not None:
try:
self._tmp_data = user_input
return await self.async_step_mqtt()
except Exception:
errors["base"] = "unknown"
return self.async_show_form(
step_id="user",
data_schema=STEP_ONE_DATA_SCHEMA,
errors=errors,
)
async def async_step_mqtt(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the MQTT topics step."""
errors: dict[str, str] = {}
if user_input is not None:
try:
data = {**self._tmp_data, **user_input}
title = DEFAULT_NAME
if data.get(CONF_HOST):
title = f"{DEFAULT_NAME} ({data[CONF_HOST]})"
return self.async_create_entry(title=title, data=data)
except Exception:
errors["base"] = "unknown"
return self.async_show_form(
step_id="mqtt",
data_schema=STEP_TWO_DATA_SCHEMA,
errors=errors,
)

View File

@@ -0,0 +1,25 @@
"""Constants for Greenhousino Irrigation integration."""
DOMAIN = "greenhousino"
MANUFACTURER = "Greenhousino"
MODEL = "Irrigation Controller"
# Defaults
DEFAULT_NAME = "Greenhousino Irrigation"
DEFAULT_COMMAND_TOPIC = "greenhousino/pump"
DEFAULT_STATE_TOPIC = "greenhousino/pumpstate"
DEFAULT_PAYLOAD_ON = "on"
DEFAULT_PAYLOAD_OFF = "off"
# Config flow keys
CONF_COMMAND_TOPIC = "command_topic"
CONF_STATE_TOPIC = "state_topic"
CONF_HOST = "host"
# Service
SERVICE_WATER_FOR = "water_for"
SERVICE_DATA_DURATION = "duration"
# State
ATTR_DURATION_MS = "duration_ms"
ATTR_PUMP_ACTIVE = "pump_active"

View File

@@ -0,0 +1,14 @@
{
"domain": "greenhousino",
"name": "Greenhousino Irrigation",
"codeowners": [],
"config_flow": true,
"dependencies": ["mqtt"],
"documentation": "https://github.com/greenhousino/irrigation",
"integration_type": "device",
"iot_class": "local_push",
"issue_tracker": "https://github.com/greenhousino/irrigation/issues",
"loggers": ["greenhousino"],
"requirements": [],
"version": "1.0.0"
}

View File

@@ -0,0 +1,14 @@
water_for:
name: Water for duration
description: Activate the irrigation pump for a specified number of seconds.
fields:
duration:
name: Duration
description: How long to water, in seconds (1-3600).
required: true
selector:
number:
min: 1
max: 3600
unit_of_measurement: "seconds"
mode: box

View File

@@ -0,0 +1,39 @@
{
"config": {
"step": {
"user": {
"title": "Greenhousino Irrigation",
"description": "Enter the IP address of your ESP irrigation controller.",
"data": {
"host": "Host IP address"
}
},
"mqtt": {
"title": "MQTT Topics",
"description": "Configure the MQTT topics for your irrigation controller.",
"data": {
"command_topic": "Command topic",
"state_topic": "State topic"
}
}
},
"error": {
"unknown": "An unknown error occurred."
},
"abort": {
"already_configured": "Device is already configured."
}
},
"services": {
"water_for": {
"name": "Water for duration",
"description": "Activate the irrigation pump for a specified number of seconds.",
"fields": {
"duration": {
"name": "Duration",
"description": "How long to water, in seconds."
}
}
}
}
}

View File

@@ -0,0 +1,151 @@
"""Switch platform for Greenhousino Irrigation."""
import logging
from typing import Any
from homeassistant.components import mqtt
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import (
ATTR_PUMP_ACTIVE,
DOMAIN,
MANUFACTURER,
MODEL,
)
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Greenhousino switch from config entry."""
hass_data = entry.runtime_data
async_add_entities(
[
GreenhousinoPumpSwitch(
entry,
hass_data.command_topic,
hass_data.state_topic,
)
]
)
class GreenhousinoPumpSwitch(SwitchEntity):
"""Representation of a Greenhousino pump switch."""
_attr_has_entity_name = True
_attr_name = "Pump"
_attr_should_poll = False
_attr_unique_id = None # will be set from entry_id
_entry_id = None
def __init__(
self,
entry: ConfigEntry,
command_topic: str,
state_topic: str,
) -> None:
"""Initialize the switch."""
self._command_topic = command_topic
self._state_topic = state_topic
self._entry_id = entry.entry_id
self._attr_unique_id = f"{entry.entry_id}-pump"
self._attr_available = False
self._state = False
@property
def device_info(self):
"""Return device info."""
return {
"identifiers": {(DOMAIN, self._entry_id)},
"manufacturer": MANUFACTURER,
"model": MODEL,
}
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
entry = self.config_entry
self.async_on_remove(
mqtt.async_subscribe(
self.hass,
self._state_topic,
self._state_received,
qos=1,
)
)
# Publish availability via MQTT discovery-style
# The device publishes to greenhousino/pump/status with "offline" on disconnect
status_topic = self._state_topic.replace("pumpstate", "pump/status")
self.async_on_remove(
mqtt.async_subscribe(
self.hass,
status_topic,
self._availability_received,
qos=1,
)
)
def _state_received(self, msg: mqtt.MqttReceivePayload) -> None:
"""Handle new MQTT state message."""
payload = msg.decode().strip()
self._state = payload == "on"
self._attr_available = True
self.async_write_ha_state()
_LOGGER.debug("State received: %s -> pump_active=%s", payload, self._state)
def _availability_received(self, msg: mqtt.MqttReceivePayload) -> None:
"""Handle availability message."""
payload = msg.decode().strip()
self._attr_available = payload != "offline"
self.async_write_ha_state()
_LOGGER.debug("Availability: %s", payload)
@property
def is_on(self) -> bool:
"""Return True if the pump is on."""
return self._state
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return device state attributes."""
return {
ATTR_PUMP_ACTIVE: self._state,
"command_topic": self._command_topic,
"state_topic": self._state_topic,
"manufacturer": MANUFACTURER,
"model": MODEL,
}
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the pump on."""
await mqtt.async_publish(
self.hass,
self._command_topic,
"on",
qos=1,
retain=False,
)
self._state = True
self.async_write_ha_state()
async def async_turn_off(self) -> None:
"""Turn the pump off."""
await mqtt.async_publish(
self.hass,
self._command_topic,
"off",
qos=1,
retain=False,
)
self._state = False
self.async_write_ha_state()

View File

@@ -0,0 +1,39 @@
{
"config": {
"step": {
"user": {
"title": "Greenhousino Irrigation",
"description": "Enter the IP address of your ESP irrigation controller.",
"data": {
"host": "Host IP address"
}
},
"mqtt": {
"title": "MQTT Topics",
"description": "Configure the MQTT topics for your irrigation controller.",
"data": {
"command_topic": "Command topic",
"state_topic": "State topic"
}
}
},
"error": {
"unknown": "An unknown error occurred."
},
"abort": {
"already_configured": "Device is already configured."
}
},
"services": {
"water_for": {
"name": "Water for duration",
"description": "Activate the irrigation pump for a specified number of seconds.",
"fields": {
"duration": {
"name": "Duration",
"description": "How long to water, in seconds."
}
}
}
}
}

View File

@@ -126,7 +126,8 @@ void reconnect()
{ {
Serial.println("INFO: Attempting MQTT connection..."); Serial.println("INFO: Attempting MQTT connection...");
// Attempt to connect // Attempt to connect
if (mqttClient.connect(MQTT_CLIENT_ID, MQTT_CLIENT_USER, MQTT_CLIENT_PW)) if (mqttClient.connect(MQTT_CLIENT_ID, MQTT_CLIENT_USER, MQTT_CLIENT_PW,
MQTT_LAST_WILL_TOPIC, 1, true, MQTT_LAST_WILL_MSG))
{ {
Serial.println("INFO: connected"); Serial.println("INFO: connected");
@@ -136,6 +137,9 @@ void reconnect()
mqttClient.publish(MQTT_STATE_TOPIC, MQTT_OFF); mqttClient.publish(MQTT_STATE_TOPIC, MQTT_OFF);
} }
// Signal online to Home Assistant availability tracker
mqttClient.publish(MQTT_LAST_WILL_TOPIC, "online");
mqttClient.subscribe(MQTT_TF_TOPIC); mqttClient.subscribe(MQTT_TF_TOPIC);
} else } else
{ {