Added guntamatic heater integration#167419
Added guntamatic heater integration#167419JensTimmerman wants to merge 49 commits intohome-assistant:devfrom
Conversation
There was a problem hiding this comment.
Hello @JensTimmerman,
When attempting to inspect the commits of your pull request for CLA signature status among all authors we encountered commit(s) which were not linked to a GitHub account, thus not allowing us to determine their status(es).
The commits that are missing a linked GitHub account are the following:
e372a09eed47377a30a1547881add2350275b0bf- This commit has something that looks like an email address (git@caret.be). Maybe try linking that to GitHub?.
Unfortunately, we are unable to accept this pull request until this situation is corrected.
Here are your options:
-
If you had an email address set for the commit that simply wasn't linked to your GitHub account you can link that email now and it will retroactively apply to your commits. The simplest way to do this is to click the link to one of the above commits and look for a blue question mark in a blue circle in the top left. Hovering over that bubble will show you what email address you used. Clicking on that button will take you to your email address settings on GitHub. Just add the email address on that page and you're all set. GitHub has more information about this option in their help center.
-
If you didn't use an email address at all, it was an invalid email, or it's one you can't link to your GitHub, you will need to change the authorship information of the commit and your global Git settings so this doesn't happen again going forward. GitHub provides some great instructions on how to change your authorship information in their help center.
- If you only made a single commit you should be able to run
(substituting "Author Name" and "
git commit --amend --author="Author Name <email@address.com>"email@address.com" for your actual information) to set the authorship information. - If you made more than one commit and the commit with the missing authorship information is not the most recent one you have two options:
- You can re-create all commits missing authorship information. This is going to be the easiest solution for developers that aren't extremely confident in their Git and command line skills.
- You can use this script that GitHub provides to rewrite history. Please note: this should be used only if you are very confident in your abilities and understand its impacts.
- Whichever method you choose, I will come by to re-check the pull request once you push the fixes to this branch.
- If you only made a single commit you should be able to run
We apologize for this inconvenience, especially since it usually bites new contributors to Home Assistant. We hope you understand the need for us to protect ourselves and the great community we all have built legally. The best thing to come out of this is that you only need to fix this once and it benefits the entire Home Assistant and GitHub community.
Thanks, I look forward to checking this PR again soon! ❤️
|
Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍 |
There was a problem hiding this comment.
Pull request overview
This pull request introduces the Guntamatic Sensor integration, which allows Home Assistant to monitor sensors from Guntamatic heaters (wood/pellet boilers). The integration uses the guntamatic Python library to fetch sensor data such as boiler temperature, buffer status, and pump states through a config flow that only requires the heater's hostname or IP address.
Changes:
- New Guntamatic sensor integration with support for dynamic sensor discovery from the device
- Config flow for easy setup via UI with connection validation
- Full test coverage for the config flow including error handling
- Proper integration setup with coordinator-based data updates
- Branding and configuration files aligned with Home Assistant standards
Reviewed changes
Copilot reviewed 16 out of 18 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| homeassistant/components/guntamatic_sensor/ | New integration with config flow, sensor entity definitions, and constants |
| tests/components/guntamatic_sensor/ | Test files for config flow validation and error handling |
| mypy.ini | Type checking configuration for the new integration |
| homeassistant/generated/ | Auto-generated integration registry and config flow files |
| requirements_all.txt, requirements_test_all.txt | Dependency declarations for guntamatic library |
| CODEOWNERS | Added codeowner for the new integration |
| .strict-typing | Added to strict typing list for the integration |
|
I added the email adres to github, not sure why the cla-error label isn't removed, since sla-signed has been added I see? |
| return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS) | ||
|
|
||
|
|
||
| async def async_remove_config_entry_device( |
| ) | ||
|
|
||
|
|
||
| async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: |
There was a problem hiding this comment.
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"
|
| def _check_sensors() -> None: | ||
| current_sensors = set(coordinator.data) | ||
| new_sensors = current_sensors - known_sensors | ||
| if new_sensors: | ||
| known_sensors.update(new_sensors) | ||
| async_add_entities( | ||
| GuntamaticSensor(coordinator, name, heater.host) for name in new_sensors | ||
| ) | ||
|
|
||
| _check_sensors() |
There was a problem hiding this comment.
Let's keep dynamic devices for a later PR
There was a problem hiding this comment.
The entire guntamatic python library is about dynamic sensors, there is no fixed list, since they could be different per device. I could remove the dynamic adding of new sensors, but there will be no difference in code lines
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Guntamatic sensors from config entry."""
data = entry.runtime_data
coordinator = data.coordinator
heater = data.heater
# Create one entity per sensor
sensors = [
GuntamaticSensor(coordinator, name, heater.host) for name in coordinator.data
]
async_add_entities(sensors)There was a problem hiding this comment.
Can you elaborate on that? Like ideally we have a list of entity descriptions in HA we match with, so we can properly contextualise the entire, for example adding translations
There was a problem hiding this comment.
At the moment I only have access to one guntamatic, it's model has 2 urls, one with pure data, and 1 with the description of the data
The data provided is stable for my specific unit, but I don't know what other values might be availabel, e.g. some models have support for automatic woodchip/pellet inpellers.
So my initial choice was not to hardcode any specific values, just query the api and return what it provides.
However it does provide some empty values for things that are not connected. e.g. the motherboard has support for 8 heating circuits pumps and 3 auxiliary pumps and always returns information for all of these (most which are always OFF since only 2 are connected in my unit)
I also recently learned from a technician that all models share the same motherboard, so they probably all return the same data.
If you say it's better to first hardcode now the most usefull values that are acutally usefull and then wait for feedback from people requesting more values I'm ok with hardcoding them all here.
For a full ist of values I get now, see https://pypi.org/project/guntamatic/
There was a problem hiding this comment.
A different problem, and initial instigator for the way I started doing this is that the 'api' also returns diferent names depending on the language the device is set in,
Mine supports ;Deutsch;English;Espaniol;Francais;Italiano;Czech;Slovenian;Hungarian;Nederlands;
So if I wanted to explicitly map these and not have them dynamic I would have to also map them in all these languages? And then again provide translation strings for all of them in all the other languages? or is there a system in place for this?
There was a problem hiding this comment.
I will wrap around these issues in the pypi library and have a few fixed sensors here,
There was a problem hiding this comment.
done, made the api always return a fixed set of values, hardcoded EntityDescriptions for these values.
Translation is in place in the api now, works for english, I will add more languages later, this won't require a change in HA.
| """Initialize the sensor.""" | ||
| super().__init__(coordinator) | ||
| self._name = name | ||
| self._attr_has_entity_name = True |
There was a problem hiding this comment.
Can be set outside of the constructor
| serial = coordinator.data.get("Serial", [None])[0] or host | ||
|
|
||
| self._attr_unique_id = ( | ||
| f"guntamatic_{serial.replace('.', '_')}_{name.replace(' ', '_')}" |
There was a problem hiding this comment.
No need to add the domain to the unique id
| unit = coordinator.data[name][1] | ||
| self._attr_native_unit_of_measurement = unit | ||
| self._attr_device_class = SENSOR_DEVICE_CLASSES.get(name) | ||
| self._attr_state_class = SENSOR_STATE_CLASSES.get(name) | ||
|
|
||
| self._attr_entity_category = ( | ||
| EntityCategory.DIAGNOSTIC if name in DIAGNOSTIC_SENSORS else None | ||
| ) |
There was a problem hiding this comment.
Can we use entity descriptions instead?
There was a problem hiding this comment.
I looked into this, I would define a list of SensorEntityDescriptions and use the ones based on unit.
e.g.
UNIT_TO_DESCRIPTION: dict[str, SensorEntityDescription] = {
"°C": SensorEntityDescription(
key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement="°C",
),
"%": SensorEntityDescription(
key="percentage",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement="%",
),
"h": SensorEntityDescription(
key="duration_hours",
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement="h",
),
"d": SensorEntityDescription(
key="duration_days",
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement="d",
),
}
I noticed other integrations are doing the same; e.g. these integrations do something similar in mapping units to device_class and state_class or usint a unit to sensorEntityDescription: vicare, fronius (huge list of SensorEntityDescrpitions filling the sensors file) rainforest_eagle, rainforest_raven, swiss_hydrological_data, ...
So I was wondering, might I not try and iplement a helper function (Factory method) in SensorEntityDescription that generates the boilerplate SensorEntityDescriptons that would make most sense, e.g.
e.g.
SensorEntityDescription.from_unit("°C")
would return a SensorEntityDescipriton with device_class=SensorDeviceClass.TEMPERATURE,;
state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement="°C",
I believe lots of sensors might be able to easily use these then?
There was a problem hiding this comment.
redone in a different way after making library return fixed values.
| # serial might not be set on all devices | ||
| serial = coordinator.data.get("Serial", [None])[0] or host |
There was a problem hiding this comment.
A host is not a serial and a host should not be used for the device identifiers
There was a problem hiding this comment.
removed, made serial required in coordinator
erwindouna
left a comment
There was a problem hiding this comment.
Making very nice progress here, @JensTimmerman! Some few quick points I saw. :)
| heater = Heater(entry.data[CONF_HOST]) | ||
|
|
||
| coordinator = GuntamaticCoordinator(hass, heater, entry) |
There was a problem hiding this comment.
| heater = Heater(entry.data[CONF_HOST]) | |
| coordinator = GuntamaticCoordinator(hass, heater, entry) | |
| coordinator = GuntamaticCoordinator(hass, Heater(entry.data[CONF_HOST]), entry) |
| serial = data["serial"][0] | ||
| await self.async_set_unique_id(serial) |
There was a problem hiding this comment.
| serial = data["serial"][0] | |
| await self.async_set_unique_id(serial) | |
| await self.async_set_unique_id(data["serial"][0]) |
| serial = data["serial"][0] | ||
| await self.async_set_unique_id(serial) |
There was a problem hiding this comment.
| serial = data["serial"][0] | |
| await self.async_set_unique_id(serial) | |
| await self.async_set_unique_id(data["serial"][0]) |
| sensors = [ | ||
| GuntamaticSensor(coordinator, description) | ||
| for description in GUNTAMATIC_SENSORS | ||
| if description.key in coordinator.data | ||
| ] | ||
|
|
||
| async_add_entities(sensors) |
There was a problem hiding this comment.
| sensors = [ | |
| GuntamaticSensor(coordinator, description) | |
| for description in GUNTAMATIC_SENSORS | |
| if description.key in coordinator.data | |
| ] | |
| async_add_entities(sensors) | |
| async_add_entities([ | |
| GuntamaticSensor(coordinator, description) | |
| for description in GUNTAMATIC_SENSORS | |
| if description.key in coordinator.data | |
| ]) |
| class GuntamaticConfigFlow(ConfigFlow, domain=DOMAIN): | ||
| """Handle a config flow for guntamatic.""" | ||
|
|
||
| VERSION = 1 |
There was a problem hiding this comment.
| VERSION = 1 |
Since it's new, it will default to 1. :)
|
|
||
| if user_input is not None: | ||
| try: | ||
| heater = Heater(user_input[CONF_HOST]) |
There was a problem hiding this comment.
I think this can be placed outside the try except, right before it. Only place parts that we're trying and might crash.
There was a problem hiding this comment.
or I could inline it in the next line?
There was a problem hiding this comment.
that could work, but I think keeping it out is a bit cleaner
| native_unit_of_measurement=UnitOfTemperature.CELSIUS, | ||
| ), | ||
| SensorEntityDescription( | ||
| key="room_0_temperature", |
There was a problem hiding this comment.
Maybe you explained it already, but just checking: the room 0, 1 and 2 are default property for each installation? What do they represent? :)
There was a problem hiding this comment.
These are temperature sensors in different rooms in the building, typically 3 wire thermostats. They don't need to be in every installation, I also noticed mine starts counting at 1 and I only have 2.
But the guntamatic will always return these values yes. If they are not connected they show up as 60degrees C. I'm planning on filtering out sensors with these values in the library. (I already have a check that these don't get added if they are not in the output data of the library: key not in coordinator.data in the loop that creates the sensors.
There are a bunch more sensors the device returns (most of which are not relevant to me or diagnostic) I started with only these because they are relevant to me, but I will add more in the future in a separate pr (or do you want me to add them all now?)
There was a problem hiding this comment.
So, if we are going to add climate entities, I assume we are going to create one for every room, is there still a need for these entities? Again, we can split them off this PR to discuss later
There was a problem hiding this comment.
according tot he documentation A climate entity controls temperature, humidity, or fans, ...
Yes, the guntamatic controls temperature, but at the moment I have no way of adding control in Home Assistant to a guntamatic device. I would need to figure out how to do this, there is talks on the internet about getting a key from the manufacturer; but they do not respond to my requests.
So at the moment this is purely sensor readouts for me.
If in the future I find a way to control it, and have a way to be able to set values via the network I can make climate entities, but I was not planing on that in the short run.
Or would you be fine with adding climate entities that only report temperature in HA?
Same for water heater entity, just sensors at the moment.
| result["flow_id"], | ||
| {CONF_HOST: "1.1.1.1"}, | ||
| ) | ||
| await hass.async_block_till_done() |
There was a problem hiding this comment.
| await hass.async_block_till_done() |
| result["flow_id"], | ||
| {CONF_HOST: "1.1.1.1"}, | ||
| ) | ||
| await hass.async_block_till_done() |
There was a problem hiding this comment.
| await hass.async_block_till_done() |
|
|
||
| async def async_setup_entry(hass: HomeAssistant, entry: GuntamaticConfigEntry) -> bool: | ||
| """Set up guntamatic from a config entry.""" | ||
| coordinator = GuntamaticCoordinator(hass, Heater(entry.data[CONF_HOST]), entry) |
There was a problem hiding this comment.
Might as well just make the Heater object in the coordinator
| except NoSerialException: | ||
| return self.async_abort(reason="bad_data") | ||
|
|
||
| # set serial as unique id for deduplication, ip isn't a good match |
There was a problem hiding this comment.
| # set serial as unique id for deduplication, ip isn't a good match |
|
|
||
| if user_input is not None: | ||
| try: | ||
| heater = Heater(user_input[CONF_HOST]) |
There was a problem hiding this comment.
that could work, but I think keeping it out is a bit cleaner
| }, | ||
| { | ||
| "registered_devices": true |
There was a problem hiding this comment.
| }, | |
| { | |
| "registered_devices": true |
Your devices don't set the mac address (yet)
There was a problem hiding this comment.
right, I read the documentation on this wrong.
| SensorEntityDescription( | ||
| key="Status", | ||
| entity_category=EntityCategory.DIAGNOSTIC, | ||
| ), |
There was a problem hiding this comment.
Lets try to snake_case them and make them an enum device class, we can also consider to remove it from the initial PR and we can discuss this in a later one
| native_unit_of_measurement=UnitOfTemperature.CELSIUS, | ||
| ), | ||
| SensorEntityDescription( | ||
| key="room_0_temperature", |
There was a problem hiding this comment.
So, if we are going to add climate entities, I assume we are going to create one for every room, is there still a need for these entities? Again, we can split them off this PR to discuss later
| [ | ||
| GuntamaticSensor(coordinator, description) | ||
| for description in GUNTAMATIC_SENSORS | ||
| if description.key in coordinator.data | ||
| ] |
There was a problem hiding this comment.
| [ | |
| GuntamaticSensor(coordinator, description) | |
| for description in GUNTAMATIC_SENSORS | |
| if description.key in coordinator.data | |
| ] | |
| GuntamaticSensor(coordinator, description) | |
| for description in GUNTAMATIC_SENSORS | |
| if description.key in coordinator.data |
| "name": "Boiler Temperature" | ||
| }, | ||
| "buffer_bottom_temperature": { | ||
| "name": "Buffer Bottem Temperature" | ||
| }, | ||
| "buffer_center_temperature": { | ||
| "name": "Buffer Center Temperature" | ||
| }, | ||
| "buffer_load": { | ||
| "name": "Buffer Load" | ||
| }, | ||
| "buffer_top_temperature": { | ||
| "name": "Buffer Top Temperature" | ||
| }, | ||
| "domestic_home_water_temperature": { | ||
| "name": "Domestic Home Water Temperature" | ||
| }, | ||
| "outdoor_temperature": { | ||
| "name": "Outdoor Temperature" | ||
| }, | ||
| "program": { | ||
| "name": "Program", | ||
| "state": { | ||
| "dhw": "Domestic Hot Water", | ||
| "dhw_boost": "Domestic Hot Water Boost", | ||
| "heat": "Heat", | ||
| "hibernate": "Hibernate", | ||
| "hibernate_to": "Hibernate To", | ||
| "off": "[%key:common::state::off%]", | ||
| "timer": "Timer" | ||
| } | ||
| }, |
joostlek
left a comment
There was a problem hiding this comment.
Feel free to message me on Discord
| with patch( | ||
| "homeassistant.components.guntamatic.Heater", | ||
| autospec=True, | ||
| ) as mock: |
There was a problem hiding this comment.
You can combine this one with the config_flow one to make sure you only need a single mock, check mealie for an example
| return MockConfigEntry( | ||
| domain=DOMAIN, | ||
| data={CONF_HOST: "1.1.1.1"}, | ||
| ) |
|
|
||
|
|
||
| @pytest.fixture | ||
| def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: |
| mock.return_value.get_data = MagicMock(return_value=MOCK_DATA) | ||
| mock.return_value.parse_data = MagicMock(return_value=MOCK_PARSE_DATA) | ||
| mock.return_value.host = "1.1.1.1" | ||
| yield mock |
There was a problem hiding this comment.
| mock.return_value.get_data = MagicMock(return_value=MOCK_DATA) | |
| mock.return_value.parse_data = MagicMock(return_value=MOCK_PARSE_DATA) | |
| mock.return_value.host = "1.1.1.1" | |
| yield mock | |
| instance = mock.return_value | |
| instance.get_data = MagicMock(return_value=MOCK_DATA) | |
| instance.parse_data = MagicMock(return_value=MOCK_PARSE_DATA) | |
| instance.host = "1.1.1.1" | |
| yield instance |
| assert result["type"] is FlowResultType.CREATE_ENTRY | ||
| assert result["title"] == "Guntamatic Heater" | ||
| assert result["data"] == {CONF_HOST: "1.1.1.1"} |
There was a problem hiding this comment.
Let's also add a test where an entry is setup and the IP changes
There was a problem hiding this comment.
done in test_dhcp_updates_ip
There was a problem hiding this comment.
I'd consider these sensor tests
There was a problem hiding this comment.
moved to sensor and added coordinator test
| await mock_config_entry.runtime_data.async_refresh() | ||
| await hass.async_block_till_done() | ||
|
|
||
| state = hass.states.get("sensor.mock_title_boiler_temperature") | ||
| assert state.state == STATE_UNAVAILABLE | ||
|
|
||
| # Recovery | ||
| mock_heater.return_value.parse_data.side_effect = None | ||
| mock_heater.return_value.parse_data.return_value = MOCK_PARSE_DATA | ||
| await mock_config_entry.runtime_data.async_refresh() |
There was a problem hiding this comment.
we shouldn't touch the runtime_data to refresh, instead use the freezer to skip time and let it happen "naturally"
There was a problem hiding this comment.
done, I looked at other FreezeGun usages and tried to copy
| mock_config_entry.add_to_hass(hass) | ||
| await hass.config_entries.async_setup(mock_config_entry.entry_id) | ||
| await hass.async_block_till_done() |
There was a problem hiding this comment.
you can use your method in __init__.py for this
| async def test_unload_entry( | ||
| hass: HomeAssistant, | ||
| mock_config_entry: MockConfigEntry, | ||
| mock_heater: MagicMock, | ||
| ) -> None: | ||
| """Test unloading the integration.""" | ||
| mock_config_entry.add_to_hass(hass) | ||
| await hass.config_entries.async_setup(mock_config_entry.entry_id) | ||
| await hass.async_block_till_done() | ||
| assert await hass.config_entries.async_unload(mock_config_entry.entry_id) | ||
| assert mock_config_entry.state is ConfigEntryState.NOT_LOADED |
There was a problem hiding this comment.
Can be combined with the first test
| data: dict[str, list[str]] = await self.hass.async_add_executor_job( | ||
| self.heater.parse_data | ||
| ) | ||
| except requests.exceptions.ConnectionError as err: |
There was a problem hiding this comment.
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.
| except requests.exceptions.ConnectionError as err: | |
| except requests.exceptions.RequestException as err: |
joostlek
left a comment
There was a problem hiding this comment.
Tests are failing, can you take a look?
| "name": "Boiler temperature" | ||
| }, | ||
| "buffer_bottom_temperature": { | ||
| "name": "Buffer bottem temperature" |
There was a problem hiding this comment.
Fix the typo "bottem" to "bottom" in the entity name so the translated sensor name is spelled correctly.
| "name": "Buffer bottem temperature" | |
| "name": "Buffer bottom temperature" |
| GUNTAMATIC_SENSORS: list[SensorEntityDescription] = [ | ||
| SensorEntityDescription( | ||
| key="program", | ||
| translation_key="program", | ||
| device_class=SensorDeviceClass.ENUM, |
There was a problem hiding this comment.
Add a sensor description for the heater "status" (or remove its translation key) so the integration’s declared translations match the entities actually created from coordinator data.
| result = await hass.config_entries.flow.async_init( | ||
| DOMAIN, context={"source": SOURCE_USER} | ||
| ) | ||
| mock_heater.parse_data.side_effect = (side_effect,) |
There was a problem hiding this comment.
Set MagicMock.side_effect directly to an exception instance (not a 1-tuple) so parse_data actually raises and the config-flow error handling is exercised.
| mock_heater.parse_data.side_effect = (side_effect,) | |
| mock_heater.parse_data.side_effect = side_effect |
| mock_heater: MagicMock, | ||
| ) -> None: | ||
| """Test DHCP discovery shows confirmation form.""" | ||
| mock_heater.parse_data.side_effect = (side_effect,) |
There was a problem hiding this comment.
Set MagicMock.side_effect directly to an exception instance (not a 1-tuple) so parse_data actually raises and the DHCP error handling path is exercised.
| mock_heater.parse_data.side_effect = ( | ||
| requests.exceptions.ConnectionError("Connection lost"), |
There was a problem hiding this comment.
Set MagicMock.side_effect to the ConnectionError instance (not a 1-tuple) so the coordinator update actually fails and last_update_success is set correctly.
| mock_heater.parse_data.side_effect = ( | |
| requests.exceptions.ConnectionError("Connection lost"), | |
| mock_heater.parse_data.side_effect = requests.exceptions.ConnectionError( | |
| "Connection lost" |
| "side_effect", | ||
| [ | ||
| requests.exceptions.ConnectionError("Connection lost"), | ||
| NoSerialException, |
There was a problem hiding this comment.
Use an exception instance for NoSerialException in the parametrized side_effects (passing the class is treated as callable and won’t be raised), otherwise this test may not cover the intended failure mode.
| NoSerialException, | |
| NoSerialException(), |
| try: | ||
| data: dict[str, list[str]] = await self.hass.async_add_executor_job( | ||
| self.heater.parse_data | ||
| ) | ||
| except requests.exceptions.ConnectionError as err: | ||
| raise UpdateFailed(f"Cannot connect to heater: {err}") from err | ||
| return data |
There was a problem hiding this comment.
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.
Proposed change
Add a new integration: Guntamatic heater, this allows one to see the data of their guntamatic heater (and connected pumps, buffer vats, underfloor heating/radiators, temp sensors...) it's very usefull for other automations to be able to use this data.
It uses the guntamatic python library to talk to it, https://pypi.org/project/guntamatic/
Type of change
Additional information
Bronze
action-setup- Service actions are registered in async_setupappropriate-polling- If it's a polling integration, set an appropriate polling intervalbrands- Has branding assets available for the integrationcommon-modules- Place common patterns in common modulesconfig-flow-test-coverage- Full test coverage for the config flowconfig-flow- Integration needs to be able to be set up via the UIdata_descriptionto give context to fieldsConfigEntry.dataandConfigEntry.optionscorrectlydependency-transparency- Dependency transparencydocs-actions- The documentation describes the provided service actions that can be useddocs-high-level-description- The documentation includes a high-level description of the integration brand, product, or servicedocs-installation-instructions- The documentation provides step-by-step installation instructions for the integration, including, if needed, prerequisitesdocs-removal-instructions- The documentation provides removal instructionsentity-event-setup- Entity events are subscribed in the correct lifecycle methodsentity-unique-id- Entities have a unique IDhas-entity-name- Entities use has_entity_name = Trueruntime-data- Use ConfigEntry.runtime_data to store runtime datatest-before-configure- Test a connection in the config flowtest-before-setup- Check during integration initialization if we are able to set it up correctlyunique-config-entry- Don't allow the same device or service to be able to be set up twiceChecklist
ruff format homeassistant tests)If user exposed functionality or configuration variables are added/changed:
If the code communicates with devices, web services, or third-party tools:
Updated and included derived files by running:
python3 -m script.hassfest.requirements_all.txt.Updated by running
python3 -m script.gen_requirements_all.To help with the load of incoming pull requests: