Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@ -248,6 +248,7 @@ homeassistant.components.gpsd.*
homeassistant.components.greeneye_monitor.*
homeassistant.components.group.*
homeassistant.components.guardian.*
homeassistant.components.guntamatic.*
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.

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

from __future__ import annotations

import logging

from homeassistant.const import Platform
from homeassistant.core import HomeAssistant

from .coordinator import GuntamaticConfigEntry, GuntamaticCoordinator

_LOGGER = logging.getLogger(__name__)
_PLATFORMS: list[Platform] = [Platform.SENSOR]


async def async_setup_entry(hass: HomeAssistant, entry: GuntamaticConfigEntry) -> bool:
"""Set up guntamatic from a config entry."""
coordinator = GuntamaticCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = 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)
92 changes: 92 additions & 0 deletions homeassistant/components/guntamatic/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""Config flow for the guntamatic integration."""

from __future__ import annotations

import logging
from typing import Any

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

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo

from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)

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


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

_discovered_ip: str

async def async_step_dhcp(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle DHCP discovery."""
heater = Heater(discovery_info.ip)
try:
data = await self.hass.async_add_executor_job(heater.parse_data)
except requests.exceptions.RequestException:
return self.async_abort(reason="cannot_connect")
except NoSerialException:
return self.async_abort(reason="bad_data")

await self.async_set_unique_id(data["serial"][0])
self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip})
self._discovered_ip = discovery_info.ip

return await self.async_step_discovery_confirm()

async def async_step_discovery_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm discovery."""
if user_input is not None:
return self.async_create_entry(
title="Guntamatic Heater",
data={CONF_HOST: self._discovered_ip},
)

self._set_confirm_only()
return self.async_show_form(step_id="discovery_confirm")
Comment thread
JensTimmerman marked this conversation as resolved.

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:
try:
heater = Heater(user_input[CONF_HOST])
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.

I think this can be placed outside the try except, right before it. Only place parts that we're trying and might crash.

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.

or I could inline it in the next line?

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.

that could work, but I think keeping it out is a bit cleaner

data = await self.hass.async_add_executor_job(heater.parse_data)
except requests.exceptions.RequestException:
errors["base"] = "cannot_connect"
except NoSerialException:
errors["base"] = "bad_data"

except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
Comment thread
JensTimmerman marked this conversation as resolved.
else:
# set serial as unique id for deduplication, ip isn't a good match
await self.async_set_unique_id(data["serial"][0])
self._abort_if_unique_id_configured()

return self.async_create_entry(
title="Guntamatic Heater", data=user_input
)

return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
6 changes: 6 additions & 0 deletions homeassistant/components/guntamatic/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Constants for the guntamatic integration."""

from datetime import timedelta

DOMAIN = "guntamatic"
SCAN_INTERVAL = timedelta(seconds=30)
44 changes: 44 additions & 0 deletions homeassistant/components/guntamatic/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""Coordinator for Guntamatic integration."""

from __future__ import annotations

import logging

from guntamatic.heater import Heater
import requests

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import DOMAIN, SCAN_INTERVAL

_LOGGER = logging.getLogger(__name__)

type GuntamaticConfigEntry = ConfigEntry[GuntamaticCoordinator]


class GuntamaticCoordinator(DataUpdateCoordinator[dict[str, list[str]]]):
Comment thread
JensTimmerman marked this conversation as resolved.
"""Guntamatic data coordinator."""

def __init__(self, hass: HomeAssistant, entry: GuntamaticConfigEntry) -> None:
"""Initialize coordinator."""
super().__init__(
hass,
logger=_LOGGER,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
config_entry=entry,
)
self.heater = Heater(entry.data[CONF_HOST])

async def _async_update_data(self) -> dict[str, list[str]]:
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.

I think it's a good practice to add an _async_setup for the config entry setup.

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.

is there any documentation about this?

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.

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.

oh, must have mist that, yeah that cleans things up a bit

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.

I am not sure what the reason was you redirected for this

Copy link
Copy Markdown
Author

@JensTimmerman JensTimmerman Apr 16, 2026

Choose a reason for hiding this comment

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

@joostlek is this remark for me or for @erwindouna ?
more context here: #167419 (comment)

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.

"""Fetch data from heater."""
try:
data: dict[str, list[str]] = await self.hass.async_add_executor_job(
self.heater.parse_data
)
except requests.exceptions.ConnectionError as err:
Comment thread
JensTimmerman marked this conversation as resolved.
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

Handle all request-related errors by raising UpdateFailed to avoid noisy stack traces on transient failures. _async_update_data currently only wraps requests.exceptions.ConnectionError, so other requests.exceptions.RequestException subclasses will bubble up and be logged as unexpected errors by the coordinator.

Suggested change
except requests.exceptions.ConnectionError as err:
except requests.exceptions.RequestException as err:

Copilot uses AI. Check for mistakes.
raise UpdateFailed(f"Cannot connect to heater: {err}") from err
Comment thread
JensTimmerman marked this conversation as resolved.
return data
Comment on lines +38 to +44
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

Handle all request/parse errors in the coordinator by raising UpdateFailed.

_async_update_data currently only wraps requests.exceptions.ConnectionError, but the same parse_data call is treated as potentially raising any requests.exceptions.RequestException (and NoSerialException) in the config flow. If those occur during polling, they will be logged as unexpected exceptions (with tracebacks) on every update. Catching requests.exceptions.RequestException (and any expected library exceptions) and re-raising as UpdateFailed will keep failures cleanly classified as communication/update problems.

Copilot uses AI. Check for mistakes.
18 changes: 18 additions & 0 deletions homeassistant/components/guntamatic/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"domain": "guntamatic",
"name": "Guntamatic",
"codeowners": ["@JensTimmerman"],
"config_flow": true,
"dependencies": [],
"dhcp": [
{
"hostname": "kessel*",
"macaddress": "0024BD*"
}
],
"documentation": "https://www.home-assistant.io/integrations/guntamatic",
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "silver",
"requirements": ["guntamatic==1.5.0"]
}
72 changes: 72 additions & 0 deletions homeassistant/components/guntamatic/quality_scale.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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: todo
discovery-update-info: done
discovery: done
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:
status: exempt
comment: Single device
entity-category: done
entity-device-class: done
entity-disabled-by-default: todo
entity-translations: done
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