Skip to content
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

Palazzetti integration #128259

Merged
merged 20 commits into from
Oct 28, 2024
Merged

Palazzetti integration #128259

merged 20 commits into from
Oct 28, 2024

Conversation

dotvav
Copy link
Contributor

@dotvav dotvav commented Oct 12, 2024

Breaking change

No breaking changes

Proposed change

New integration for Palazzetti stoves

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New integration (thank you!)
  • New feature (which adds functionality to an existing integration)
  • Deprecation (breaking change to happen in the future)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

Checklist

  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • I have followed the perfect PR recommendations
  • The code has been formatted using Ruff (ruff format homeassistant tests)
  • Tests have been added to verify that the new code works.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description.

To help with the load of incoming pull requests:

@dotvav
Copy link
Contributor Author

dotvav commented Oct 12, 2024

Not sure about the 2 failed checks:

  • The license audit is failing for palazzetti-sdk-local-api, its github page mentions the MIT license
  • The tests that fail don't seem to be related to my changes

@IceBotYT
Copy link
Contributor

It looks like it's failing because the MIT license isn't present in setup.py.

@joostlek joostlek marked this pull request as draft October 13, 2024 10:53
Copy link
Member

@zweckj zweckj left a comment

Choose a reason for hiding this comment

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

First feedback, note your snapshot test is also failing.

homeassistant/components/palazzetti/__init__.py Outdated Show resolved Hide resolved
homeassistant/components/palazzetti/__init__.py Outdated Show resolved Hide resolved
homeassistant/components/palazzetti/climate.py Outdated Show resolved Hide resolved
homeassistant/components/palazzetti/coordinator.py Outdated Show resolved Hide resolved
homeassistant/components/palazzetti/strings.json Outdated Show resolved Hide resolved
tests/components/palazzetti/test_config_flow.py Outdated Show resolved Hide resolved
tests/components/palazzetti/test_config_flow.py Outdated Show resolved Hide resolved
homeassistant/components/palazzetti/climate.py Outdated Show resolved Hide resolved
homeassistant/components/palazzetti/climate.py Outdated Show resolved Hide resolved
homeassistant/components/palazzetti/climate.py Outdated Show resolved Hide resolved
@dotvav dotvav marked this pull request as ready for review October 16, 2024 16:33
@dotvav dotvav requested a review from zweckj October 16, 2024 17:43
homeassistant/components/palazzetti/__init__.py Outdated Show resolved Hide resolved
homeassistant/components/palazzetti/__init__.py Outdated Show resolved Hide resolved
homeassistant/components/palazzetti/__init__.py Outdated Show resolved Hide resolved
homeassistant/components/palazzetti/__init__.py Outdated Show resolved Hide resolved
homeassistant/components/palazzetti/climate.py Outdated Show resolved Hide resolved
homeassistant/components/palazzetti/config_flow.py Outdated Show resolved Hide resolved
Comment on lines 46 to 49
formatted_mac = dr.format_mac(palazzetti.mac)

# Assign a unique ID to the flow
await self.async_set_unique_id(formatted_mac)
Copy link
Member

Choose a reason for hiding this comment

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

Can you give an example of the mac? If you can find it using a tool like https://www.wireshark.org/tools/oui-lookup.html, you can use dhcp discovery

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. I wanted to introduce dhcp discovery in a subsequent PR, when this MVP is completed.

homeassistant/components/palazzetti/coordinator.py Outdated Show resolved Hide resolved
Comment on lines 63 to 64
LOGGER.exception(err)
available = False
Copy link
Member

Choose a reason for hiding this comment

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

If its not available we should rather just raise UpdateFailed

Comment on lines 66 to 76
data: PalazzettiData = {
AVAILABLE: available,
IS_HEATING: self.palazzetti.is_heating,
TARGET_TEMPERATURE: self.palazzetti.target_temperature,
ROOM_TEMPERATURE: self.palazzetti.room_temperature,
OUTLET_TEMPERATURE: self.palazzetti.outlet_temperature,
EXHAUST_TEMPERATURE: self.palazzetti.exhaust_temperature,
PELLET_QUANTITY: self.palazzetti.pellet_quantity,
FAN_SPEED: self.palazzetti.fan_speed,
}
return data
Copy link
Member

Choose a reason for hiding this comment

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

You can also return nothing and have the entities directly reference self.coordinator.palazzetti (or rename to client for ease of access)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks! I was slightly confused about maintaining state at this level and this makes much more sense.

@home-assistant home-assistant bot marked this pull request as draft October 16, 2024 18:35
@home-assistant
Copy link

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

@dotvav dotvav marked this pull request as ready for review October 17, 2024 12:02
@home-assistant home-assistant bot requested a review from joostlek October 17, 2024 12:02
@dotvav
Copy link
Contributor Author

dotvav commented Oct 19, 2024

@zweckj @joostlek I think I have addressed all your comments.

Comment on lines 21 to 25
try:
await coordinator.client.connect()
await coordinator.client.update_state()
except (CommunicationError, ValidationError) as err:
raise ConfigEntryNotReady from err
Copy link
Member

Choose a reason for hiding this comment

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

can be moved to _async_setup in the coordinator (which will be called automatically during async_config_entry_first_refresh), making this a bit cleaner

Comment on lines 40 to 48
if coordinator.client.connected:
entities.append(
PalazzettiClimateEntity(
coordinator=coordinator,
)
)
async_add_entities(entities)
else:
raise ConfigEntryNotReady
Copy link
Member

Choose a reason for hiding this comment

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

is this really necessary? wouldn't we raise during the connection if devices are unavailable? If we can add the devices add them, they will show unavailble anyways

"""List the hvac modes."""
if self.coordinator.client.has_on_off_switch:
return [HVACMode.OFF, HVACMode.HEAT]
return [self.hvac_mode]
Copy link
Member

Choose a reason for hiding this comment

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

what does this mean? if the device has no switch it's either always on or off? if it's always off does this make sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The on/off switch becomes unavaiables at times, during ignition and shut off cycles. An API call to turn on/off during that time will not raise an error but it will fail.

^ adding this as a comment for clarity

Copy link
Member

Choose a reason for hiding this comment

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

The climate should then always return both modes and we should just handle the error gracefully, letting the user know what's going on (raising HomeAssistantError)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But the API it letting me know that this action is not available at that moment. Why let the user take it and get back an error?

Copy link
Member

Choose a reason for hiding this comment

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

Because then you can let the user know why it's not working instead of the option just disappearing. I believe we are usually don't change supported features at runtime.

class PalazzettiConfigFlow(ConfigFlow, domain=DOMAIN):
"""Palazzetti config flow."""

VERSION = 1
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
VERSION = 1

default

self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""User configuration step."""
if user_input and CONF_HOST in user_input:
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if user_input and CONF_HOST in user_input:
if user_input is not None:

host is required, so this is all we need to check for

Comment on lines 23 to 39
with (
patch(
"homeassistant.components.palazzetti.config_flow.PalazzettiClient.connect",
return_value=True,
) as mock_connect,
patch(
"homeassistant.components.palazzetti.config_flow.PalazzettiClient.update_state",
return_value=True,
) as mock_update_state,
patch(
"homeassistant.components.palazzetti.config_flow.PalazzettiClient.mac",
return_value="11:22:33:44:55:66",
) as mock_mac,
patch(
"homeassistant.components.palazzetti.config_flow.PalazzettiClient.name",
return_value="stove",
) as mock_name,
Copy link
Member

Choose a reason for hiding this comment

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

is there a reason you're not using your fixture for that?

Comment on lines 53 to 55
assert len(mock_update_state.mock_calls) == 0
assert len(mock_mac.mock_calls) > 0
assert len(mock_name.mock_calls) > 0
Copy link
Member

Choose a reason for hiding this comment

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

do those add value?

Comment on lines 72 to 73
assert result.get("type") is FlowResultType.FORM
assert result.get("errors") == {"base": "invalid_host"}
Copy link
Member

Choose a reason for hiding this comment

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

get rid of the gets

await hass.config_entries.async_unload(mock_config_entry.entry_id)
await hass.async_block_till_done()

assert not hass.data.get(DOMAIN)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
assert not hass.data.get(DOMAIN)

Comment on lines 148 to 153
if fan_mode == FAN_SILENT:
await self.coordinator.client.set_fan_silent()
elif fan_mode == FAN_HIGH:
await self.coordinator.client.set_fan_high()
elif fan_mode == FAN_AUTO:
await self.coordinator.client.set_fan_auto()
Copy link
Member

Choose a reason for hiding this comment

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

those can probably throw, right?

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
Copy link
Member

Choose a reason for hiding this comment

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

Use the extended ConfigEntry

_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = DOMAIN

def __init__(self, *, coordinator: PalazzettiDataUpdateCoordinator) -> None:
Copy link
Member

Choose a reason for hiding this comment

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

Personal opinion, having the coordinator as kwargs doesn't add much imo

"""Defines a Palazzetti climate."""

_attr_has_entity_name = True
_attr_hvac_modes = [] # The available modes will be set when we know the current state
Copy link
Member

Choose a reason for hiding this comment

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

Don't initialize mutable objects like dicts or lists on class level. They will be shared with every instance of this class, causing side effects

"""Initialize Palazzetti climate."""
super().__init__(coordinator=coordinator)
client = coordinator.client
self._attr_unique_id = f"{coordinator.config_entry.data[CONF_MAC]}-climate"
Copy link
Member

Choose a reason for hiding this comment

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

unique ids are unique per domain per platform, so unless you are going to add more climates, the suffix isn't needed

client = coordinator.client
self._attr_unique_id = f"{coordinator.config_entry.data[CONF_MAC]}-climate"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, coordinator.config_entry.data[CONF_MAC])},
Copy link
Member

Choose a reason for hiding this comment

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

Use connections instead to allow the device to be merged with other discovered devices (from integrations like unifi that create devices for devices found on the network)

Comment on lines 137 to 138
temperature = cast(float, kwargs.get(ATTR_TEMPERATURE))
await self.coordinator.client.set_target_temperature(int(temperature))
Copy link
Member

Choose a reason for hiding this comment

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

why do we cast to a float and then int it?

await self.coordinator.client.set_fan_auto()
else:
await self.coordinator.client.set_fan_speed(FAN_MODES.index(fan_mode))
except (CommunicationError, ValidationError) as err:
Copy link
Member

Choose a reason for hiding this comment

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

What's a validation error?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The API may throw a ValidationError if the argument is out of bounds

Copy link
Member

Choose a reason for hiding this comment

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

But is that something the user should act on or something temporary? In that case we should raise ServiceValidationError

# Abort the flow if a config entry with the same unique ID exists
self._abort_if_unique_id_configured()

name = palazzetti.name
Copy link
Member

Choose a reason for hiding this comment

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

Ah so the name comes from the device? Why do we store that in the config entry instead of fetching it at runtime?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No need. Removing.

except (CommunicationError, ValidationError) as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err

async def _async_setup(self) -> None:
Copy link
Member

Choose a reason for hiding this comment

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

Rather put this above the _async_update_data


async def _async_setup(self) -> None:
try:
await self.client.connect()
Copy link
Member

Choose a reason for hiding this comment

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

Do we need to disconnect somewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, the connect method is getting some static device config/properties. The underlying is an http GET request. It needs to be called once (at least), and there are no connections to close.

@home-assistant home-assistant bot marked this pull request as draft October 20, 2024 13:20
@dotvav dotvav marked this pull request as ready for review October 20, 2024 18:35
@home-assistant home-assistant bot requested review from joostlek and zweckj October 20, 2024 18:35
Comment on lines 84 to 92
@property
def supported_features(self) -> ClimateEntityFeature:
"""Supported features."""
return (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.TURN_ON
| ClimateEntityFeature.TURN_OFF
)
Copy link
Member

Choose a reason for hiding this comment

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

This can be moved to _attr_supported_features = ...

try:
await self.coordinator.client.set_on(hvac_mode != HVACMode.OFF)
except CommunicationError as err:
raise HomeAssistantError from err
Copy link
Member

Choose a reason for hiding this comment

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

Can we add more context to this error?

translation_domain=DOMAIN,
translation_key=TEMPERATURE_OUT_OF_BOUNDS,
translation_placeholders={
"value": f"{temperature}",
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"value": f"{temperature}",
"value": str(temperature),

DOMAIN: Final = "palazzetti"
PALAZZETTI: Final = "Palazzetti"
LOGGER = logging.getLogger(__package__)
SCAN_INTERVAL = timedelta(seconds=5)
Copy link
Member

Choose a reason for hiding this comment

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

I find this to be a very low scan interval. I don't believe that a heater changes every 5 seconds. If you want the device to be responsive after an action has been done, be sure to call self.coordinator.async_refresh() to get a new state

await self.client.connect()
await self.client.update_state()
except (CommunicationError, ValidationError) as err:
raise ConfigEntryNotReady from err
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
raise ConfigEntryNotReady from err
raise UpdateFailed("...") from err

"config": {
"step": {
"user": {
"description": "Enter the host name or the IP address of the Palazzetti CBox",
Copy link
Member

Choose a reason for hiding this comment

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

Rather use data_description to add this to the config flow

Comment on lines 11 to 15
"abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
},
Copy link
Member

Choose a reason for hiding this comment

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

Both are not used. Instead, you need already_configured

},
"exceptions": {
"on_off_not_available": {
"message": "The appliance cannot be turned on or off atthe moment. Try again later."
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"message": "The appliance cannot be turned on or off atthe moment. Try again later."
"message": "The appliance cannot be turned on or off at the moment. Try again later."

"message": "The appliance cannot be turned on or off atthe moment. Try again later."
},
"fan_speed_out_of_bounds": {
"message": "This fan mode ({value}) is not supported."
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"message": "This fan mode ({value}) is not supported."
"message": "Fan mode {value} is not supported."

"message": "This fan mode ({value}) is not supported."
},
"target_temperature_out_of_bounds": {
"message": "This target temperature ({value}) is not supported."
Copy link
Member

Choose a reason for hiding this comment

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

out of bounds is different than not supported imo

@home-assistant home-assistant bot marked this pull request as draft October 21, 2024 13:16
@dotvav dotvav marked this pull request as ready for review October 23, 2024 08:16
@home-assistant home-assistant bot requested a review from joostlek October 23, 2024 08:16
Copy link
Member

@joostlek joostlek left a comment

Choose a reason for hiding this comment

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

final things on the normal code

mac = self.coordinator.config_entry.data[CONF_MAC]
self._attr_unique_id = mac
self._attr_device_info = dr.DeviceInfo(
identifiers={(DOMAIN, mac)},
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
identifiers={(DOMAIN, mac)},

Can be removed as connections is also fine

Comment on lines 41 to 63
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = DOMAIN

def __init__(self, *kargs, **kwargs) -> None:
"""Initialize Palazzetti climate."""
super().__init__(*kargs, **kwargs)
client = self.coordinator.client
mac = self.coordinator.config_entry.data[CONF_MAC]
self._attr_unique_id = mac
self._attr_device_info = dr.DeviceInfo(
identifiers={(DOMAIN, mac)},
connections={(dr.CONNECTION_NETWORK_MAC, mac)},
name=client.name,
manufacturer=PALAZZETTI,
sw_version=client.sw_version,
hw_version=client.hw_version,
)
self._attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.TURN_ON
| ClimateEntityFeature.TURN_OFF
)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = DOMAIN
def __init__(self, *kargs, **kwargs) -> None:
"""Initialize Palazzetti climate."""
super().__init__(*kargs, **kwargs)
client = self.coordinator.client
mac = self.coordinator.config_entry.data[CONF_MAC]
self._attr_unique_id = mac
self._attr_device_info = dr.DeviceInfo(
identifiers={(DOMAIN, mac)},
connections={(dr.CONNECTION_NETWORK_MAC, mac)},
name=client.name,
manufacturer=PALAZZETTI,
sw_version=client.sw_version,
hw_version=client.hw_version,
)
self._attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.TURN_ON
| ClimateEntityFeature.TURN_OFF
)
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = DOMAIN
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.TURN_ON
| ClimateEntityFeature.TURN_OFF
)
def __init__(self, *kargs, **kwargs) -> None:
"""Initialize Palazzetti climate."""
super().__init__(*kargs, **kwargs)
client = self.coordinator.client
mac = self.coordinator.config_entry.data[CONF_MAC]
self._attr_unique_id = mac
self._attr_device_info = dr.DeviceInfo(
identifiers={(DOMAIN, mac)},
connections={(dr.CONNECTION_NETWORK_MAC, mac)},
name=client.name,
manufacturer=PALAZZETTI,
sw_version=client.sw_version,
hw_version=client.hw_version,
)

Comment on lines +93 to +95
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="cannot_connect"
) from err
Copy link
Member

Choose a reason for hiding this comment

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

Can we add this to the other CommuncationErrors as well?

_attr_name = None
_attr_target_temperature_step = 1.0
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = DOMAIN
Copy link
Member

Choose a reason for hiding this comment

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

Why does this have a translation key

Copy link
Contributor Author

Choose a reason for hiding this comment

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

absolutely no reason

@home-assistant home-assistant bot marked this pull request as draft October 23, 2024 12:07
CONF_HOST: "127.0.0.1",
CONF_MAC: "11:22:33:44:55:66",
},
unique_id="unique_id",
Copy link
Member

Choose a reason for hiding this comment

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

Can we use a proper unique_id?

Comment on lines 20 to 22
CONF_NAME: "Stove",
CONF_HOST: "127.0.0.1",
CONF_MAC: "11:22:33:44:55:66",
Copy link
Member

Choose a reason for hiding this comment

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

the data we create the entry with now has changed

Comment on lines 29 to 36
def mock_palazzetti():
"""Return a mocked PalazzettiClient."""
with (
patch(
"homeassistant.components.palazzetti.coordinator.PalazzettiClient",
AsyncMock,
) as client,
):
Copy link
Member

Choose a reason for hiding this comment

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

Recommend checking out how we patch the Airgradient client in airgradient. We patch both the normal client as the config flow ones. This gets rid of the patch in the test_config_flow.py

mock_palazzetti: AsyncMock,
) -> None:
"""Test the creation and values of Palazzetti climate device."""
patch("pypalazzetti.client.PalazzettiClient", mock_palazzetti)
Copy link
Member

Choose a reason for hiding this comment

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

With the last comment, this would not be needed

Comment on lines 26 to 34
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()

assert mock_config_entry.state is ConfigEntryState.LOADED
assert entity_registry.async_is_registered(CLIMATE_ID)

entity = entity_registry.async_get(CLIMATE_ID)

assert entity.unique_id == "11:22:33:44:55:66"
Copy link
Member

Choose a reason for hiding this comment

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

I would recommend looking into snapshot testing. Check flexit_bacnet or airgradient as example. You can just have the == snapshot and then run the tests like pytest ./tests/components/palazzetti --snapshot-update and it will generate the .ambr files which fixate the entity.

mock_palazzetti,
) as mock_client,
):
result2 = await hass.config_entries.flow.async_configure(
Copy link
Member

Choose a reason for hiding this comment

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

No need to create result2, since you never use result afterwards anyway, might as well overwrite (personal opinion)

user_input={CONF_HOST: "192.168.1.1"},
)

assert result2["type"] is FlowResultType.CREATE_ENTRY
Copy link
Member

Choose a reason for hiding this comment

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

We should also patch the async_setup_entry, since otherwise the integration will just start up here, causing lingering test tasks. Please check the mock_setup_entry fixture that is frequently used.

user_input={CONF_HOST: "192.168.1.1"},
)

assert result2["type"] is FlowResultType.CREATE_ENTRY
Copy link
Member

Choose a reason for hiding this comment

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

We should assert more here. Check the name of the entry, the unique_id, the data in the result

Comment on lines 38 to 53
async def test_invalid_host(hass: HomeAssistant) -> None:
"""Test we handle cannot connect error."""
with (
patch(
"homeassistant.components.palazzetti.coordinator.PalazzettiClient.connect",
side_effect=CommunicationError(),
),
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
data={CONF_HOST: "192.168.1.1"},
)

assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "cannot_connect"}
Copy link
Member

Choose a reason for hiding this comment

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

All the tests should end in either ABORT or CREATE_ENTRY. This way we test the flow is able to recover from an error.

So we should fix the patch and make sure it can pass and it creates sucessfuly

Copy link
Member

Choose a reason for hiding this comment

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

The test I am missing is where you add the mock_config_entry and then try to setup the same device again. it should abort the flow

@dotvav dotvav marked this pull request as ready for review October 26, 2024 19:22
@home-assistant home-assistant bot requested a review from joostlek October 26, 2024 19:22
@joostlek
Copy link
Member

Aight, I pulled the code and I fixed the last things. You now will have translated fan modes, better tests (I removed the other example, I think airgradient isn't the best example anymore, but in that case I have 2 different devices and then we have something different to test, which is different from the same but different data) and a test for the device

@joostlek joostlek merged commit 8eb68b5 into home-assistant:dev Oct 28, 2024
40 of 42 checks passed
@github-actions github-actions bot locked and limited conversation to collaborators Oct 29, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants