-
-
Notifications
You must be signed in to change notification settings - Fork 37.4k
Added guntamatic heater integration #167419
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
e372a09
3d7c7b7
67150e6
d77f1ad
d73e6b0
1c3e485
8499ddd
d6ff64c
f08bddb
1e06298
c3ebb1a
a422c0b
8b9235b
0c12805
604a845
3c4c97e
3892ada
f7b606c
6c4ddfd
29fd735
4a313f8
3c27e88
943e7c5
f8c0130
5b724e7
7c7f3bc
9950e6e
c82bf5e
ca98c9d
7dfde24
ec42fec
eecd590
8e5cbc9
75eea53
a05ff73
32006fe
f08775f
80ddaf8
cf4787e
a776434
1d47520
7b0360e
d68f468
b364784
5e47527
2f7b88f
aa1c8c8
39fe372
940b44e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| 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) |
| 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") | ||
|
|
||
| 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]) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. or I could inline it in the next line?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
|
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 | ||
| ) | ||
| 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) |
| 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]]]): | ||||||
|
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]]: | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's a good practice to add an
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is there any documentation about this?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh, must have mist that, yeah that cleans things up a bit
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure what the reason was you redirected for this
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @joostlek is this remark for me or for @erwindouna ?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||||||
|
JensTimmerman marked this conversation as resolved.
|
||||||
| except requests.exceptions.ConnectionError as err: | |
| except requests.exceptions.RequestException as err: |
Copilot
AI
Apr 28, 2026
There was a problem hiding this comment.
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.
| 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"] | ||
| } |
| 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 |
Uh oh!
There was an error while loading. Please reload this page.