Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
58bacbb
Fix identifier generation for sub devices in ViCare (#154330)
CFenner Oct 13, 2025
15e9965
Bump github/codeql-action from 4.30.7 to 4.30.8 (#154357)
dependabot[bot] Oct 13, 2025
ddfa6f3
Bump opower to 0.15.7 (#154351)
tronikos Oct 13, 2025
e4ea798
Add support for μg/m³ for Carbon Monoxide (#153158)
wittypluck Oct 13, 2025
c7d7cfa
Add Reolink IO input binary sensor (#154133)
starkillerOG Oct 13, 2025
3a71087
Bump aioasuswrt to 1.5.1 (#153209)
kennedyshead Oct 13, 2025
d389405
Bump PyViCare to 2.54.0 (#154336)
CFenner Oct 13, 2025
e081155
update pysqueezebox lib to 0.13.0 (#154358)
wollew Oct 13, 2025
1182082
Bump actions/dependency-review-action from 4.8.0 to 4.8.1 (#154356)
dependabot[bot] Oct 13, 2025
76a0b2d
Bump renault-api to 0.4.4 (#154137)
epenet Oct 13, 2025
a3b67d5
Add support to sensor statistics for changing unit_class (#154130)
emontnemery Oct 13, 2025
2512dad
Set model_id in miele integration (#154367)
astrandb Oct 13, 2025
54ff491
Implement MAC address exclude list in nmap_tracker (#142724)
tedvdb Oct 13, 2025
3c3b4ef
Fix stale docstring in nmap_tracker (#154380)
emontnemery Oct 13, 2025
f1e0954
Automatically setup hardware integrations when firmware info is publi…
puddly Oct 13, 2025
694b169
Bump aioairq to 0.4.7 (#154386)
Sibgatulin Oct 13, 2025
8943321
Bump hass-nabucasa from 1.2.0 to 1.3.0 (#154376)
klejejs Oct 13, 2025
cca5c80
Store nmap tracker options as lists (#154378)
emontnemery Oct 13, 2025
8aa5e7d
Bump momonga to 0.2.0 (#154371)
SeraphicRav Oct 13, 2025
3151384
Set integration type for Satel Integra to device (#154372)
Tommatheussen Oct 13, 2025
d7aa083
Bump airOS preparing for model_id matching (#154370)
CoMPaTech Oct 13, 2025
ce1fdc6
Update xknx to 3.10.0 (#154361)
farmio Oct 13, 2025
6f79a65
AsusWRT: Pass only online clients to the device list from the API (#1…
Vaskivskyi Oct 13, 2025
a991dcb
Add Bluetooth API to clear address from match history (#154355)
bdraco Oct 13, 2025
eab1205
Add config flow title placeholder update infrastructure (#154353)
bdraco Oct 13, 2025
2d0b4dd
New Foscam switch (#152732)
Foscam-wangzhengyu Oct 13, 2025
f185ffd
Set model_id on device for Volvo integration (#154385)
thomasddn Oct 13, 2025
94d015e
Fix Bluetooth discovery for devices with alternating advertisement na…
bdraco Oct 13, 2025
bf273ef
Add integration_type to airOS (#154390)
CoMPaTech Oct 13, 2025
f649717
Add model_id support to airOS (#154388)
CoMPaTech Oct 13, 2025
6f8439d
Fix switch platform for Comelit SimpleHome (#154227)
chemelli74 Oct 13, 2025
1452aec
Add switch platform to Nintendo Parental controls integration (#154179)
pantherale0 Oct 13, 2025
dd6bc71
Add switch platform and grid charge enable for Growatt Server integra…
johanzander Oct 13, 2025
f0756af
Add Python version file (#154267)
balloob Oct 13, 2025
04f83bc
Add actron_air climate integration (#134740)
kclif9 Oct 13, 2025
01dee65
Add 14 additional sensor entities for Growatt TLX/MIN inverters (#153…
johanzander Oct 13, 2025
0bfdd70
async_config_entry_first_refresh in update coordinator requires a con…
gjohansson-ST Oct 13, 2025
97afec1
Record `last_reported` for KNX BinarySensor entitiy states (#154392)
farmio Oct 13, 2025
82c536a
Migrate Matter descriptions to be `kw_only` (#154398)
TheJulianJES Oct 13, 2025
1e5f5f4
Enable pylint consider-math-not-float check (#154338)
cdce8p Oct 13, 2025
4fb3c9f
Add async_update_and_abort method to config flow (#153146)
gjohansson-ST Oct 13, 2025
5708f61
Prepare to move out URL's from MQTT translation strings (#154391)
jbouwh Oct 13, 2025
b7718f6
Bump aiocomelit to 1.1.2 (#154393)
chemelli74 Oct 13, 2025
610183c
Fix Improv BLE factory reset rediscovery (#154354)
bdraco Oct 13, 2025
663431f
Allow following of 302 redirects in generic camera (#154308)
davet2001 Oct 13, 2025
324aa09
Update Improv BLE discovery notification when device name changes (#1…
bdraco Oct 13, 2025
90a0262
Bump aioesphomeapi to 41.15.0 (#154407)
bdraco Oct 13, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,7 @@ jobs:
steps:
- *checkout
- name: Dependency review
uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 # v4.8.0
uses: actions/dependency-review-action@40c09b7dc99638e5ddb0bfd91c1673effc064d8a # v4.8.1
with:
license-check: false # We use our own license audit checks

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0

- name: Initialize CodeQL
uses: github/codeql-action/init@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
with:
languages: python

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
with:
category: "/language:python"
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ junit.xml
.project
.pydevproject

.python-version
.tool-versions

# emacs auto backups
Expand Down
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
2 changes: 2 additions & 0 deletions CODEOWNERS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

USER vscode

ENV UV_PYTHON=3.13.2
COPY .python-version ./
RUN uv python install

ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
Expand Down
57 changes: 57 additions & 0 deletions homeassistant/components/actron_air/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""The Actron Air integration."""

from actron_neo_api import (
ActronAirNeoACSystem,
ActronNeoAPI,
ActronNeoAPIError,
ActronNeoAuthError,
)

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

from .const import _LOGGER
from .coordinator import (
ActronAirConfigEntry,
ActronAirRuntimeData,
ActronAirSystemCoordinator,
)

PLATFORM = [Platform.CLIMATE]


async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> bool:
"""Set up Actron Air integration from a config entry."""

api = ActronNeoAPI(refresh_token=entry.data[CONF_API_TOKEN])
systems: list[ActronAirNeoACSystem] = []

try:
systems = await api.get_ac_systems()
await api.update_status()
except ActronNeoAuthError:
_LOGGER.error("Authentication error while setting up Actron Air integration")
raise
except ActronNeoAPIError as err:
_LOGGER.error("API error while setting up Actron Air integration: %s", err)
raise

system_coordinators: dict[str, ActronAirSystemCoordinator] = {}
for system in systems:
coordinator = ActronAirSystemCoordinator(hass, entry, api, system)
_LOGGER.debug("Setting up coordinator for system: %s", system["serial"])
await coordinator.async_config_entry_first_refresh()
system_coordinators[system["serial"]] = coordinator

entry.runtime_data = ActronAirRuntimeData(
api=api,
system_coordinators=system_coordinators,
)

await hass.config_entries.async_forward_entry_setups(entry, PLATFORM)
return True


async def async_unload_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORM)
259 changes: 259 additions & 0 deletions homeassistant/components/actron_air/climate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
"""Climate platform for Actron Air integration."""

from typing import Any

from actron_neo_api import ActronAirNeoStatus, ActronAirNeoZone

from homeassistant.components.climate import (
FAN_AUTO,
FAN_HIGH,
FAN_LOW,
FAN_MEDIUM,
ClimateEntity,
ClimateEntityFeature,
HVACMode,
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import DOMAIN
from .coordinator import ActronAirConfigEntry, ActronAirSystemCoordinator

PARALLEL_UPDATES = 0

FAN_MODE_MAPPING_ACTRONAIR_TO_HA = {
"AUTO": FAN_AUTO,
"LOW": FAN_LOW,
"MED": FAN_MEDIUM,
"HIGH": FAN_HIGH,
}
FAN_MODE_MAPPING_HA_TO_ACTRONAIR = {
v: k for k, v in FAN_MODE_MAPPING_ACTRONAIR_TO_HA.items()
}
HVAC_MODE_MAPPING_ACTRONAIR_TO_HA = {
"COOL": HVACMode.COOL,
"HEAT": HVACMode.HEAT,
"FAN": HVACMode.FAN_ONLY,
"AUTO": HVACMode.AUTO,
"OFF": HVACMode.OFF,
}
HVAC_MODE_MAPPING_HA_TO_ACTRONAIR = {
v: k for k, v in HVAC_MODE_MAPPING_ACTRONAIR_TO_HA.items()
}


async def async_setup_entry(
hass: HomeAssistant,
entry: ActronAirConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Actron Air climate entities."""
system_coordinators = entry.runtime_data.system_coordinators
entities: list[ClimateEntity] = []

for coordinator in system_coordinators.values():
status = coordinator.data
name = status.ac_system.system_name
entities.append(ActronSystemClimate(coordinator, name))

entities.extend(
ActronZoneClimate(coordinator, zone)
for zone in status.remote_zone_info
if zone.exists
)

async_add_entities(entities)


class BaseClimateEntity(CoordinatorEntity[ActronAirSystemCoordinator], ClimateEntity):
"""Base class for Actron Air climate entities."""

_attr_has_entity_name = True
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.TURN_ON
| ClimateEntityFeature.TURN_OFF
)
_attr_name = None
_attr_fan_modes = list(FAN_MODE_MAPPING_ACTRONAIR_TO_HA.values())
_attr_hvac_modes = list(HVAC_MODE_MAPPING_ACTRONAIR_TO_HA.values())

def __init__(
self,
coordinator: ActronAirSystemCoordinator,
name: str,
) -> None:
"""Initialize an Actron Air unit."""
super().__init__(coordinator)
self._serial_number = coordinator.serial_number


class ActronSystemClimate(BaseClimateEntity):
"""Representation of the Actron Air system."""

_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.TURN_ON
| ClimateEntityFeature.TURN_OFF
)

def __init__(
self,
coordinator: ActronAirSystemCoordinator,
name: str,
) -> None:
"""Initialize an Actron Air unit."""
super().__init__(coordinator, name)
serial_number = coordinator.serial_number
self._attr_unique_id = serial_number
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, serial_number)},
name=self._status.ac_system.system_name,
manufacturer="Actron Air",
model_id=self._status.ac_system.master_wc_model,
sw_version=self._status.ac_system.master_wc_firmware_version,
serial_number=serial_number,
)

@property
def min_temp(self) -> float:
"""Return the minimum temperature that can be set."""
return self._status.min_temp

@property
def max_temp(self) -> float:
"""Return the maximum temperature that can be set."""
return self._status.max_temp

@property
def _status(self) -> ActronAirNeoStatus:
"""Get the current status from the coordinator."""
return self.coordinator.data

@property
def hvac_mode(self) -> HVACMode | None:
"""Return the current HVAC mode."""
if not self._status.user_aircon_settings.is_on:
return HVACMode.OFF

mode = self._status.user_aircon_settings.mode
return HVAC_MODE_MAPPING_ACTRONAIR_TO_HA.get(mode)

@property
def fan_mode(self) -> str | None:
"""Return the current fan mode."""
fan_mode = self._status.user_aircon_settings.fan_mode
return FAN_MODE_MAPPING_ACTRONAIR_TO_HA.get(fan_mode)

@property
def current_humidity(self) -> float:
"""Return the current humidity."""
return self._status.master_info.live_humidity_pc

@property
def current_temperature(self) -> float:
"""Return the current temperature."""
return self._status.master_info.live_temp_c

@property
def target_temperature(self) -> float:
"""Return the target temperature."""
return self._status.user_aircon_settings.temperature_setpoint_cool_c

async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set a new fan mode."""
api_fan_mode = FAN_MODE_MAPPING_HA_TO_ACTRONAIR.get(fan_mode.lower())
await self._status.user_aircon_settings.set_fan_mode(api_fan_mode)

async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the HVAC mode."""
ac_mode = HVAC_MODE_MAPPING_HA_TO_ACTRONAIR.get(hvac_mode)
await self._status.ac_system.set_system_mode(ac_mode)

async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set the temperature."""
temp = kwargs.get(ATTR_TEMPERATURE)
await self._status.user_aircon_settings.set_temperature(temperature=temp)


class ActronZoneClimate(BaseClimateEntity):
"""Representation of a zone within the Actron Air system."""

_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TURN_ON
| ClimateEntityFeature.TURN_OFF
)

def __init__(
self,
coordinator: ActronAirSystemCoordinator,
zone: ActronAirNeoZone,
) -> None:
"""Initialize an Actron Air unit."""
super().__init__(coordinator, zone.title)
serial_number = coordinator.serial_number
self._zone_id: int = zone.zone_id
self._attr_unique_id: str = f"{serial_number}_zone_{zone.zone_id}"
self._attr_device_info: DeviceInfo = DeviceInfo(
identifiers={(DOMAIN, self._attr_unique_id)},
name=zone.title,
manufacturer="Actron Air",
model="Zone",
suggested_area=zone.title,
via_device=(DOMAIN, serial_number),
)

@property
def min_temp(self) -> float:
"""Return the minimum temperature that can be set."""
return self._zone.min_temp

@property
def max_temp(self) -> float:
"""Return the maximum temperature that can be set."""
return self._zone.max_temp

@property
def _zone(self) -> ActronAirNeoZone:
"""Get the current zone data from the coordinator."""
status = self.coordinator.data
return status.zones[self._zone_id]

@property
def hvac_mode(self) -> HVACMode | None:
"""Return the current HVAC mode."""
if self._zone.is_active:
mode = self._zone.hvac_mode
return HVAC_MODE_MAPPING_ACTRONAIR_TO_HA.get(mode)
return HVACMode.OFF

@property
def current_humidity(self) -> float | None:
"""Return the current humidity."""
return self._zone.humidity

@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
return self._zone.live_temp_c

@property
def target_temperature(self) -> float | None:
"""Return the target temperature."""
return self._zone.temperature_setpoint_cool_c

async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the HVAC mode."""
is_enabled = hvac_mode != HVACMode.OFF
await self._zone.enable(is_enabled)

async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set the temperature."""
await self._zone.set_temperature(temperature=kwargs["temperature"])
Loading
Loading