Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 29 additions & 0 deletions homeassistant/components/flexit_bacnet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue

from .const import DOMAIN
from .coordinator import FlexitConfigEntry, FlexitCoordinator

PLATFORMS: list[Platform] = [
Expand All @@ -25,6 +28,32 @@ async def async_setup_entry(hass: HomeAssistant, entry: FlexitConfigEntry) -> bo
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

# Check if deprecated fireplace switch is enabled and create repair issue
entity_reg = er.async_get(hass)
fireplace_switch_id = f"{coordinator.device.serial_number}-fireplace_mode"

# Look for the fireplace switch entity
for entity in er.async_entries_for_config_entry(entity_reg, entry.entry_id):
if entity.unique_id == fireplace_switch_id and not entity.disabled:
# Switch is enabled, create deprecation issue
climate_entity_id = entity.entity_id.replace("switch.", "climate.").replace(
"_fireplace_mode", ""
)
async_create_issue(
hass,
DOMAIN,
f"deprecated_switch_{entity.unique_id}",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_fireplace_switch",
translation_placeholders={
"entity_id": entity.entity_id,
"climate_entity_id": climate_entity_id,
},
)
break

return True


Expand Down
32 changes: 23 additions & 9 deletions homeassistant/components/flexit_bacnet/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from typing import Any

from flexit_bacnet import (
OPERATION_MODE_FIREPLACE,
OPERATION_MODE_OFF,
VENTILATION_MODE_AWAY,
VENTILATION_MODE_HOME,
VENTILATION_MODE_STOP,
Expand All @@ -12,7 +14,6 @@

from homeassistant.components.climate import (
PRESET_AWAY,
PRESET_BOOST,
PRESET_HOME,
ClimateEntity,
ClimateEntityFeature,
Expand All @@ -28,8 +29,10 @@
DOMAIN,
MAX_TEMP,
MIN_TEMP,
OPERATION_TO_PRESET_MODE_MAP,
PRESET_FIREPLACE,
PRESET_HIGH,
PRESET_TO_VENTILATION_MODE_MAP,
VENTILATION_TO_PRESET_MODE_MAP,
)
from .coordinator import FlexitConfigEntry, FlexitCoordinator
from .entity import FlexitEntity
Expand All @@ -51,6 +54,7 @@ class FlexitClimateEntity(FlexitEntity, ClimateEntity):
"""Flexit air handling unit."""

_attr_name = None
_attr_translation_key = "flexit_bacnet"

_attr_hvac_modes = [
HVACMode.OFF,
Expand All @@ -60,7 +64,8 @@ class FlexitClimateEntity(FlexitEntity, ClimateEntity):
_attr_preset_modes = [
PRESET_AWAY,
PRESET_HOME,
PRESET_BOOST,
PRESET_HIGH,
PRESET_FIREPLACE,
]

_attr_supported_features = (
Expand Down Expand Up @@ -127,20 +132,29 @@ def preset_mode(self) -> str:

Requires ClimateEntityFeature.PRESET_MODE.
"""
return VENTILATION_TO_PRESET_MODE_MAP[self.device.ventilation_mode]
return OPERATION_TO_PRESET_MODE_MAP.get(self.device.operation_mode, PRESET_HOME)
Copy link

Copilot AI Nov 3, 2025

Choose a reason for hiding this comment

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

Using PRESET_HOME as a default fallback could mask issues when an unknown operation mode is encountered. Consider logging a warning when falling back to the default, or using PRESET_NONE as a more neutral fallback to indicate an unknown state.

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

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

@copilot open a new pull request to apply changes based on this feedback


async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
ventilation_mode = PRESET_TO_VENTILATION_MODE_MAP[preset_mode]

try:
await self.device.set_ventilation_mode(ventilation_mode)
if preset_mode == PRESET_FIREPLACE:
# Use trigger method for fireplace mode
await self.device.trigger_fireplace_mode()
else:
# If currently in fireplace mode, toggle it off first
# trigger_fireplace_mode() acts as a toggle
if self.device.operation_mode == OPERATION_MODE_FIREPLACE:
await self.device.trigger_fireplace_mode()

# Set the desired ventilation mode
ventilation_mode = PRESET_TO_VENTILATION_MODE_MAP[preset_mode]
await self.device.set_ventilation_mode(ventilation_mode)
except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="set_preset_mode",
translation_placeholders={
"preset": str(ventilation_mode),
"preset": preset_mode,
},
) from exc
finally:
Expand All @@ -149,7 +163,7 @@ async def async_set_preset_mode(self, preset_mode: str) -> None:
@property
def hvac_mode(self) -> HVACMode:
"""Return hvac operation ie. heat, cool mode."""
if self.device.ventilation_mode == VENTILATION_MODE_STOP:
if self.device.operation_mode == OPERATION_MODE_OFF:
return HVACMode.OFF

return HVACMode.FAN_ONLY
Expand Down
30 changes: 18 additions & 12 deletions homeassistant/components/flexit_bacnet/const.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,40 @@
"""Constants for the Flexit Nordic (BACnet) integration."""

from flexit_bacnet import (
OPERATION_MODE_AWAY,
OPERATION_MODE_FIREPLACE,
OPERATION_MODE_HIGH,
OPERATION_MODE_HOME,
OPERATION_MODE_OFF,
VENTILATION_MODE_AWAY,
VENTILATION_MODE_HIGH,
VENTILATION_MODE_HOME,
VENTILATION_MODE_STOP,
)

from homeassistant.components.climate import (
PRESET_AWAY,
PRESET_BOOST,
PRESET_HOME,
PRESET_NONE,
)
from homeassistant.components.climate import PRESET_AWAY, PRESET_HOME, PRESET_NONE

DOMAIN = "flexit_bacnet"

MAX_TEMP = 30
MIN_TEMP = 10

VENTILATION_TO_PRESET_MODE_MAP = {
VENTILATION_MODE_STOP: PRESET_NONE,
VENTILATION_MODE_AWAY: PRESET_AWAY,
VENTILATION_MODE_HOME: PRESET_HOME,
VENTILATION_MODE_HIGH: PRESET_BOOST,
PRESET_HIGH = "high"
PRESET_FIREPLACE = "fireplace"
Comment on lines +22 to +23
Copy link

Copilot AI Nov 3, 2025

Choose a reason for hiding this comment

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

These custom preset constants should include docstring comments explaining their purpose and when they're used, following Home Assistant's documentation standards for public constants.

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

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

@copilot open a new pull request to apply changes based on this feedback


# Map operation mode (what device reports) to Home Assistant preset
OPERATION_TO_PRESET_MODE_MAP = {
OPERATION_MODE_OFF: PRESET_NONE,
OPERATION_MODE_AWAY: PRESET_AWAY,
OPERATION_MODE_HOME: PRESET_HOME,
OPERATION_MODE_HIGH: PRESET_HIGH,
OPERATION_MODE_FIREPLACE: PRESET_FIREPLACE,
}

# Map preset to ventilation mode (for setting standard modes)
PRESET_TO_VENTILATION_MODE_MAP = {
PRESET_NONE: VENTILATION_MODE_STOP,
PRESET_AWAY: VENTILATION_MODE_AWAY,
PRESET_HOME: VENTILATION_MODE_HOME,
PRESET_BOOST: VENTILATION_MODE_HIGH,
PRESET_HIGH: VENTILATION_MODE_HIGH,
}
14 changes: 14 additions & 0 deletions homeassistant/components/flexit_bacnet/icons.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
{
"entity": {
"climate": {
"flexit_bacnet": {
"state_attributes": {
"preset_mode": {
"state": {
"away": "mdi:home-export-outline",
"fireplace": "mdi:fireplace",
"high": "mdi:fan-speed-3",
"home": "mdi:home"
}
}
}
}
},
"number": {
"away_extract_fan_setpoint": {
"default": "mdi:fan-minus"
Expand Down
20 changes: 20 additions & 0 deletions homeassistant/components/flexit_bacnet/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@
"name": "Air filter polluted"
}
},
"climate": {
"flexit_bacnet": {
"state_attributes": {
"preset_mode": {
"state": {
"away": "Away",
"fireplace": "Fireplace",
"high": "High",
"home": "Home"
}
}
}
}
},
"number": {
"away_extract_fan_setpoint": {
"name": "Away extract fan setpoint"
Expand Down Expand Up @@ -139,5 +153,11 @@
"switch_turn": {
"message": "Failed to turn the switch {state}."
}
},
"issues": {
"deprecated_fireplace_switch": {
"description": "The fireplace mode switch entity `{entity_id}` is deprecated and will be removed in a future version.\n\nFireplace mode has been moved to a climate preset to better match the device interface. Please update your automations to use the climate entity preset instead.\n\n**To migrate:**\n\n1. Open your automation that uses `{entity_id}`\n2. Replace the switch service call with:\n```yaml\nservice: climate.set_preset_mode\ntarget:\n entity_id: {climate_entity_id}\ndata:\n preset_mode: fireplace\n```\n\n3. After migrating your automations, you can safely disable this switch entity.",
"title": "Fireplace mode switch is deprecated"
}
}
}
57 changes: 50 additions & 7 deletions homeassistant/components/flexit_bacnet/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.util import slugify

from .const import DOMAIN
from .coordinator import FlexitConfigEntry, FlexitCoordinator
Expand All @@ -39,20 +41,21 @@ class FlexitSwitchEntityDescription(SwitchEntityDescription):
turn_on_fn=lambda data: data.enable_electric_heater(),
turn_off_fn=lambda data: data.disable_electric_heater(),
),
FlexitSwitchEntityDescription(
key="fireplace_mode",
translation_key="fireplace_mode",
is_on_fn=lambda data: data.fireplace_ventilation_status,
turn_on_fn=lambda data: data.trigger_fireplace_mode(),
turn_off_fn=lambda data: data.trigger_fireplace_mode(),
),
FlexitSwitchEntityDescription(
key="cooker_hood_mode",
translation_key="cooker_hood_mode",
is_on_fn=lambda data: data.cooker_hood_status,
turn_on_fn=lambda data: data.activate_cooker_hood(),
turn_off_fn=lambda data: data.deactivate_cooker_hood(),
),
FlexitSwitchEntityDescription(
key="fireplace_mode",
translation_key="fireplace_mode",
entity_registry_enabled_default=False,
is_on_fn=lambda data: data.fireplace_ventilation_status,
turn_on_fn=lambda data: data.trigger_fireplace_mode(),
turn_off_fn=lambda data: data.trigger_fireplace_mode(),
),
)


Expand Down Expand Up @@ -99,6 +102,26 @@ def is_on(self) -> bool:

async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn switch on."""
# Create deprecation warning for fireplace mode switch
if self.entity_description.key == "fireplace_mode":
# Derive climate entity ID from switch entity ID
climate_entity_id = self.entity_id.replace("switch.", "climate.").replace(
"_fireplace_mode", ""
)
async_create_issue(
self.hass,
DOMAIN,
f"deprecated_switch_{slugify(self.entity_id)}",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_fireplace_switch",
translation_placeholders={
"entity_id": self.entity_id,
"climate_entity_id": climate_entity_id,
},
)

try:
await self.entity_description.turn_on_fn(self.coordinator.data)
except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc:
Expand All @@ -114,6 +137,26 @@ async def async_turn_on(self, **kwargs: Any) -> None:

async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn switch off."""
# Create deprecation warning for fireplace mode switch
if self.entity_description.key == "fireplace_mode":
# Derive climate entity ID from switch entity ID
climate_entity_id = self.entity_id.replace("switch.", "climate.").replace(
"_fireplace_mode", ""
)
async_create_issue(
self.hass,
DOMAIN,
f"deprecated_switch_{slugify(self.entity_id)}",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_fireplace_switch",
translation_placeholders={
"entity_id": self.entity_id,
"climate_entity_id": climate_entity_id,
},
)

try:
await self.entity_description.turn_off_fn(self.coordinator.data)
except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc:
Expand Down
3 changes: 3 additions & 0 deletions tests/components/flexit_bacnet/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def mock_flexit_bacnet() -> Generator[AsyncMock]:
flexit_bacnet.air_temp_setpoint_away = 18.0
flexit_bacnet.air_temp_setpoint_home = 22.0
flexit_bacnet.ventilation_mode = 4
flexit_bacnet.operation_mode = 4 # HIGH mode
flexit_bacnet.air_filter_operating_time = 8000
flexit_bacnet.outside_air_temperature = -8.6
flexit_bacnet.supply_air_temperature = 19.1
Expand All @@ -68,6 +69,8 @@ def mock_flexit_bacnet() -> Generator[AsyncMock]:
flexit_bacnet.air_filter_exchange_interval = 8784
flexit_bacnet.electric_heater = True
flexit_bacnet.fireplace_mode_runtime = 10
flexit_bacnet.fireplace_ventilation_status = False
flexit_bacnet.cooker_hood_status = False

# Mock fan setpoints
flexit_bacnet.fan_setpoint_extract_air_fire = 56
Expand Down
10 changes: 6 additions & 4 deletions tests/components/flexit_bacnet/snapshots/test_climate.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
'preset_modes': list([
'away',
'home',
'boost',
'high',
'fireplace',
]),
'target_temp_step': 0.5,
}),
Expand Down Expand Up @@ -42,7 +43,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <ClimateEntityFeature: 401>,
'translation_key': None,
'translation_key': 'flexit_bacnet',
'unique_id': '0000-0001',
'unit_of_measurement': None,
})
Expand All @@ -59,11 +60,12 @@
]),
'max_temp': 30,
'min_temp': 10,
'preset_mode': 'boost',
'preset_mode': 'high',
'preset_modes': list([
'away',
'home',
'boost',
'high',
'fireplace',
]),
'supported_features': <ClimateEntityFeature: 401>,
'target_temp_step': 0.5,
Expand Down
Loading