Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
e372a09
Added guntamatic heater integration
JensTimmerman Apr 4, 2026
3d7c7b7
Addressed copilot comments to guntamatic integration
JensTimmerman Apr 5, 2026
67150e6
Merge branch 'dev' into add_guntamatic_integration
JensTimmerman Apr 5, 2026
d77f1ad
Addresses remarks in pr
JensTimmerman Apr 5, 2026
d73e6b0
Merge branch 'dev' into add_guntamatic_integration
JensTimmerman Apr 5, 2026
1c3e485
Update tests/components/guntamatic_sensor/conftest.py
JensTimmerman Apr 5, 2026
8499ddd
Apply suggestions from code review
JensTimmerman Apr 5, 2026
d6ff64c
Apply suggestions from code review
JensTimmerman Apr 5, 2026
f08bddb
Addresses remarks in pr and fix tests
JensTimmerman Apr 5, 2026
1e06298
explicitly pass config entry
JensTimmerman Apr 5, 2026
c3ebb1a
No more dynamic sensors, hardcode sensorentity descriptions
JensTimmerman Apr 12, 2026
a422c0b
Apply suggestions from code review
JensTimmerman Apr 12, 2026
8b9235b
moved guntamatic_sensor to guntamatic
JensTimmerman Apr 12, 2026
0c12805
Fixed pr remarks
JensTimmerman Apr 12, 2026
604a845
fixed changed domain in const
JensTimmerman Apr 12, 2026
3c4c97e
fix test mocking get_data vs parse_data
JensTimmerman Apr 12, 2026
3892ada
Clean up dhcp discovery flow
JensTimmerman Apr 12, 2026
f7b606c
Clean up dhcp discovery flow
JensTimmerman Apr 12, 2026
6c4ddfd
Addressed remarks
JensTimmerman Apr 12, 2026
29fd735
Update homeassistant/components/guntamatic/strings.json
JensTimmerman Apr 12, 2026
4a313f8
Don't wrap CancelledError
JensTimmerman Apr 12, 2026
3c27e88
Merge branch 'dev' into add_guntamatic_integration
JensTimmerman Apr 13, 2026
943e7c5
Created composite and snapshot tests
JensTimmerman Apr 13, 2026
f8c0130
Bump dependency
JensTimmerman Apr 13, 2026
5b724e7
Don't raise ConfigEntryError on regular updates
JensTimmerman Apr 13, 2026
7c7f3bc
Add GuntamaticConfigEntry type
JensTimmerman Apr 13, 2026
9950e6e
Update homeassistant/components/guntamatic/__init__.py
JensTimmerman Apr 13, 2026
c82bf5e
Catch NoSerialException in init without duplicate network calls, test…
JensTimmerman Apr 13, 2026
ca98c9d
Fix test
JensTimmerman Apr 13, 2026
7dfde24
Added async setup in coordinator
JensTimmerman Apr 13, 2026
ec42fec
Code cleanup
JensTimmerman Apr 16, 2026
eecd590
Translations added
JensTimmerman Apr 19, 2026
8e5cbc9
Use serial in discovery, seperate confirm step
JensTimmerman Apr 19, 2026
75eea53
Use serial in discovery, seperate confirm step
JensTimmerman Apr 19, 2026
a05ff73
Merge branch 'dev' into add_guntamatic_integration
JensTimmerman Apr 20, 2026
32006fe
Added tests
JensTimmerman Apr 20, 2026
f08775f
Fix return type in tests
JensTimmerman Apr 20, 2026
80ddaf8
Removed unneeded async_setup
JensTimmerman Apr 20, 2026
cf4787e
updated quality scale
JensTimmerman Apr 20, 2026
a776434
Fixed remarks in pr
JensTimmerman Apr 21, 2026
1d47520
Cleanup tests
JensTimmerman Apr 24, 2026
7b0360e
Removed unneeded code
JensTimmerman Apr 24, 2026
d68f468
Fix mockconfigentry type, added translations
JensTimmerman Apr 27, 2026
b364784
Fixed sentence case in ambr file
JensTimmerman Apr 27, 2026
5e47527
More asserts on unique_id
JensTimmerman Apr 27, 2026
2f7b88f
fix typo
JensTimmerman Apr 27, 2026
aa1c8c8
Merge branch 'dev' into add_guntamatic_integration
JensTimmerman Apr 27, 2026
39fe372
Merge branch 'dev' into add_guntamatic_integration
JensTimmerman Apr 28, 2026
940b44e
Fix
joostlek Apr 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .strict-typing
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ homeassistant.components.gpsd.*
homeassistant.components.greeneye_monitor.*
homeassistant.components.group.*
homeassistant.components.guardian.*
homeassistant.components.guntamatic_sensor.*
homeassistant.components.habitica.*
homeassistant.components.hardkernel.*
homeassistant.components.hardware.*
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 38 additions & 0 deletions homeassistant/components/guntamatic_sensor/README.md
Comment thread
JensTimmerman marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Guntamatic Sensor

## High-Level Description
The Guntamatic Sensor integration allows Home Assistant to monitor sensors from Guntamatic heaters. Guntamatic is a brand of modern wood/pellet gas boilers. This integration exposes temperature, operational state, and other relevant sensors.


## Sensors
The integration currently exposes the following sensors (dynamic values):

- Boiler state: Running, STANDBY
- Boiler temperature
- Outside temperature
- Buffer load and buffer top/mid/bottom temperatures
- Boiler shunt pump, suction fan, primary and secondary air
- CO₂ content
- Domestic hot water (DHW) temperatures and pumps
- Heating circulation pumps and flow temperatures for multiple zones
- Program states (HEAT/HC)
- Interruptions
- Serial number, version, operation time, service hours
- Auxiliary pumps
- Additional WW/Buffer sensors

## Installation Instructions
1. Copy the `guntamatic_sensor` folder into `config/custom_components/`.
2. Restart Home Assistant.
3. Go to **Settings → Devices & Services → Add Integration**.
4. Search for "Guntamatic Sensor" and enter the host address of your heater.

## Removal Instructions
1. Go to **Settings → Devices & Services**.
2. Select the Guntamatic Sensor integration.
3. Click **Delete** to remove the integration and its entities.
4. Optional: Delete the `guntamatic_sensor` folder from `custom_components/`.
Comment thread
JensTimmerman marked this conversation as resolved.
Outdated

## Services
This integration does **not** provide any Home Assistant service calls.

101 changes: 101 additions & 0 deletions homeassistant/components/guntamatic_sensor/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""The guntamatic integration."""

from __future__ import annotations

from dataclasses import dataclass
import logging

from guntamatic.heater import Heater

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import DOMAIN, SCAN_INTERVAL

# For your initial PR, limit it to 1 platform.
Comment thread
JensTimmerman marked this conversation as resolved.
Outdated
_LOGGER = logging.getLogger(__name__)
_PLATFORMS: list[Platform] = [Platform.SENSOR]

type GuntamaticConfigEntry = ConfigEntry[Heater]
Comment thread
JensTimmerman marked this conversation as resolved.
Outdated


@dataclass
class GuntamaticData:
"""Data for the Guntamatic integration."""

heater: Heater
coordinator: DataUpdateCoordinator
Comment thread
JensTimmerman marked this conversation as resolved.
Outdated


async def async_setup_entry(hass: HomeAssistant, entry: GuntamaticConfigEntry) -> bool:
"""Set up guntamatic from a config entry."""

host = entry.data[CONF_HOST]
heater = Heater(host)

# initial connectivity check
initial_data = None
try:
initial_data = await hass.async_add_executor_job(heater.get_data)
except Exception as err:
raise ConfigEntryNotReady(
f"Cannot connect to Guntamatic heater: {err}"
) from err

if not initial_data:
raise ConfigEntryNotReady("Cannot connect to Guntamatic heater")

async def async_update_data() -> dict[str, list[str]]:
"""Fetch all sensor data from the heater.

Expected return format:
{
"Boiler Temperature": [68.5, "°C"],
"Flue Temperature": [115.2, "°C"],
"Power Output": [12.4, "kW"],
}
"""

data: dict[str, list[str]] = await hass.async_add_executor_job(heater.get_data)
if not data:
raise UpdateFailed("No data received from heater")
return data

coordinator = DataUpdateCoordinator(
Comment thread
JensTimmerman marked this conversation as resolved.
Outdated
hass,
logger=_LOGGER,
name="guntamatic_sensor",
update_method=async_update_data,
update_interval=SCAN_INTERVAL,
config_entry=entry,
)

coordinator.data = initial_data
entry.runtime_data = GuntamaticData(heater=heater, coordinator=coordinator)

await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
return True


async def async_unload_entry(hass: HomeAssistant, entry: GuntamaticConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)


async def async_remove_config_entry_device(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's omit this from this PR

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

hass: HomeAssistant,
config_entry: GuntamaticConfigEntry,
device_entry: dr.DeviceEntry,
) -> bool:
"""Remove a config entry from a device."""
return not any(
identifier
for identifier in device_entry.identifiers
if identifier[0] == DOMAIN
and identifier[1]
== config_entry.runtime_data.coordinator.data.get("Serial", [None])[0]
)
73 changes: 73 additions & 0 deletions homeassistant/components/guntamatic_sensor/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""Config flow for the guntamatic integration."""

from __future__ import annotations

import logging
from typing import Any

from guntamatic.heater import Heater
import requests
import voluptuous as vol

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError

from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)

STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
}
)


async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be inlined IMO

Copy link
Copy Markdown
Author

@JensTimmerman JensTimmerman Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I try ruff always complains and wants me to create an inner function:
Is there an example of what pattern to use to do this config_flow correctly?
I looked at https://developers.home-assistant.io/docs/data_entry_flow_index/#validation and this uses an external function

  TRY301 Abstract `raise` to an inner function
    --> homeassistant/components/guntamatic_sensor/config_flow.py:55:21
     |
  53 |                 heater = Heater(user_input[CONF_HOST])
  54 |                 if not await self.hass.async_add_executor_job(heater.get_data):
  55 |                     raise ConnectionError("No data received")
     |                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  56 |             except requests.exceptions.ConnectionError, ConnectionError:
  57 |                 errors["base"] = "cannot_connect"
     |


Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in a diferent way

"""Validate the user input allows us to connect.

Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
heater = Heater(data[CONF_HOST])

if not await hass.async_add_executor_job(heater.get_data):
raise CannotConnect

# Return info that you want to store in the config entry.
return {"host": data[CONF_HOST]}
Comment thread
JensTimmerman marked this conversation as resolved.
Outdated


class GuntamaticConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for guntamatic."""

VERSION = 1

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
try:
info = await validate_input(self.hass, user_input)
except CannotConnect, requests.exceptions.ConnectionError:
Comment thread
JensTimmerman marked this conversation as resolved.
Outdated
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_create_entry(
title="Guntamatic Heater",
data=info,
)

return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)


class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""
31 changes: 31 additions & 0 deletions homeassistant/components/guntamatic_sensor/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Constants for the guntamatic integration."""

from datetime import timedelta

from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass

DOMAIN = "guntamatic_sensor"
SCAN_INTERVAL = timedelta(seconds=30)
DIAGNOSTIC_SENSORS = {"Serial", "Version", "Operat. time", "Service Hrs"}

SENSOR_DEVICE_CLASSES: dict[str, SensorDeviceClass | None] = {
"Boiler temperature": SensorDeviceClass.TEMPERATURE,
"Outside Temp.": SensorDeviceClass.TEMPERATURE,
"Buffer Top": SensorDeviceClass.TEMPERATURE,
"Buffer Mid": SensorDeviceClass.TEMPERATURE,
"Buffer Btm": SensorDeviceClass.TEMPERATURE,
"DHW 0": SensorDeviceClass.TEMPERATURE,
"DHW 1": SensorDeviceClass.TEMPERATURE,
"DHW 2": SensorDeviceClass.TEMPERATURE,
# co2 content is explicitly not a co2 device class, this is flue gas, which will be 900.000ppm it doesn't make sense
# to put it in the category of indoor air quality measurement devices
# "CO2 Content": SensorDeviceClass.CO2,
"Operat. time": SensorDeviceClass.DURATION,
}

SENSOR_STATE_CLASSES: dict[str, SensorStateClass | None] = {
"Boiler temperature": SensorStateClass.MEASUREMENT,
"Outside Temp.": SensorStateClass.MEASUREMENT,
"Buffer load.": SensorStateClass.MEASUREMENT,
"CO2 Content": SensorStateClass.MEASUREMENT,
}
19 changes: 19 additions & 0 deletions homeassistant/components/guntamatic_sensor/diagnostics.py
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep diagnostics for a later PR

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Diagnostics support for Guntamatic."""

from __future__ import annotations

from typing import Any

from homeassistant.core import HomeAssistant

from . import GuntamaticConfigEntry


async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: GuntamaticConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
return {
"entry_data": dict(entry.data),
"data": entry.runtime_data.coordinator.data,
}
15 changes: 15 additions & 0 deletions homeassistant/components/guntamatic_sensor/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"domain": "guntamatic_sensor",
"name": "guntamatic",
Comment thread
JensTimmerman marked this conversation as resolved.
Outdated
"codeowners": ["@JensTimmerman"],
"config_flow": true,
"dependencies": [],
"documentation": "https://www.home-assistant.io/integrations/guntamatic_sensor",
"homekit": {},
Comment thread
JensTimmerman marked this conversation as resolved.
Outdated
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "silver",
"requirements": ["guntamatic==1.0.3"],
"ssdp": [],
"zeroconf": []
}
74 changes: 74 additions & 0 deletions homeassistant/components/guntamatic_sensor/quality_scale.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
rules:
# Bronze
action-setup: done
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
No custom actions are defined.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done

# Silver
action-exceptions:
status: exempt
comment: |
No custom actions are defined.
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: No options flow
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow:
status: exempt
comment: No authentication required.
test-coverage: done

# Gold
devices: done
diagnostics: done
discovery-update-info:
status: exempt
comment: Guntamatic heaters do not support network discovery protocols.
discovery:
status: exempt
comment: Guntamatic heaters do not support network discovery protocols.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not? What's the Mac address? What's the hostname? Have you checked mDNS?

Copy link
Copy Markdown
Author

@JensTimmerman JensTimmerman Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm intressted in having auto discovery, but the only feasible approach I found was to check every ip in a big network range and see if they had a daqdata.cgi url that returns expected data.
As far as I see this is a completly passive device that does not send any data on the local network and just responds to the 3 cgi get requests I discovered. It does not seem to have a hostname, just responds by ip.

I ran avahi-browse -a and it doesn't show up, I can find the mac address :

arp -n 192.168.1.41
Address HWtype HWaddress Flags Mask Iface
192.168.1.41 ether 00:24:bd:00:xx:xx C enp6s0u1u3

I'm not sure how unique this is:

Hainzl Industriesysteme GmbH , manages 1 unique MAC address prefix. These prefixes are part of a massive allocation with a total block size of 16.8 million addresses.

However I now noticed it does advertise a hostname of kessel0001 (german for boiler/heater)
the combination of mac prefix and kessel could be pretty unique to this device type, but how to know for sure? Is there some analytical data from HA that I could check this against?

I found http://ha:8123/config/dhcp
and that indeed shows it did discover the dhcp request from kessel001 and that mac addres.

I couldn't find a lot of more information, but could it work in some environments if I add this to manifest.json?

"dhcp": [
  {
    "hostname": "kessel*",
    "macaddress": "00:24:bd:*"
  }
]

edit:
I think I see how this worked, I recently added the unifi integration, and as this is probably a router-based device trackers, HA can now see dhcp requests, it could not in the past since the unifi gateway was not forwarding dhcp requests.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, Kessel might be very generic. It doesn't sound specific for this brand. I think I'll raise the question in the core team if a combination of the hostname with the Mac would be good as that'd make it a bit more specific.

What does hainzl do? Is it maybe the company behind this brand? Maybe that's specific enough

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I primary looked at https://developers.home-assistant.io/docs/network_discovery/ and could not find any info about this dhcp option in manifest.json, is this something I should add there?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://developers.home-assistant.io/docs/creating_integration_manifest#dhcp

Good one, didn't know it wasn't documented there, will look into that

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Author

@JensTimmerman JensTimmerman Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I can see hainzl makes the technology (motherboards) for the guntamatic boilers, so guntamatic is one of their clients: https://www.hainzl.at/en/embedded-systems-en/use-case-guntamatic/

Given they are active in the area's of Hydraulics, Elektro mechanics,,Elektronic systems & telematics,Machine & plant automation,Optimization of machine & plant energy,Handling & robotic systems,Process & spray technology,Building technology their mac adress ranges might show up in more products with api's.

kessel[0-9][0-9][0-9][0-9] might be a bit more restrictive, but I have no idea in what might show up.

There is an easy extra test: check if daqdata.cgi is hosted on port 80, but currently HA doesn't support this in the dhcp discovery?

again, does HA have analytics on mac adress ranges and hostname patterns? That could tell us a lot if these are shared somewhere to be analysed (anonymised)

docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: done
entity-category: done
entity-device-class: todo
entity-disabled-by-default: todo
entity-translations: todo
exception-translations: todo
icon-translations: todo
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo

# Platinum
async-dependency: todo
inject-websession: todo
strict-typing: todo
Loading
Loading