From 300c582ea020eb7840ae51d450fff1d696150c66 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 4 Sep 2025 00:23:45 -0400 Subject: [PATCH 01/11] Devcontainer fixes for Debian 13 (#151655) --- .devcontainer/devcontainer.json | 2 ++ Dockerfile.dev | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 085aa9c2b01201..eabe0cd500aba2 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,6 +8,8 @@ "PYTHONASYNCIODEBUG": "1" }, "features": { + // Node feature required for Claude Code until fixed https://github.com/anthropics/devcontainer-features/issues/28 + "ghcr.io/devcontainers/features/node:1": {}, "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {}, "ghcr.io/devcontainers/features/github-cli:1": {} }, diff --git a/Dockerfile.dev b/Dockerfile.dev index 4c037799567b73..c16ca2c9522685 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -3,8 +3,7 @@ FROM mcr.microsoft.com/vscode/devcontainers/base:debian SHELL ["/bin/bash", "-o", "pipefail", "-c"] RUN \ - curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ - && apt-get update \ + apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ # Additional library needed by some tests and accordingly by VScode Tests Discovery bluez \ From ed134e22f9318a77f3f401951a40a555868d5bb5 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Wed, 3 Sep 2025 23:27:12 -0700 Subject: [PATCH 02/11] Allow defining the start weekday for statistic_during_period (#149033) --- homeassistant/components/recorder/models/statistics.py | 1 + homeassistant/components/recorder/util.py | 9 ++++++++- tests/components/recorder/test_websocket_api.py | 10 ++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/models/statistics.py b/homeassistant/components/recorder/models/statistics.py index 08da12d6b178f6..be216923892408 100644 --- a/homeassistant/components/recorder/models/statistics.py +++ b/homeassistant/components/recorder/models/statistics.py @@ -78,6 +78,7 @@ class CalendarStatisticPeriod(TypedDict, total=False): period: Literal["hour", "day", "week", "month", "year"] offset: int + first_weekday: Literal["mon", "tue", "wed", "thu", "fri", "sat", "sun"] class FixedStatisticPeriod(TypedDict, total=False): diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index cff3e868def371..9876167e515796 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -27,6 +27,7 @@ from sqlalchemy.sql.lambdas import StatementLambdaElement import voluptuous as vol +from homeassistant.const import WEEKDAYS from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, issue_registry as ir from homeassistant.helpers.recorder import ( # noqa: F401 @@ -802,6 +803,7 @@ def is_second_sunday(date_time: datetime) -> bool: { vol.Required("period"): vol.Any("hour", "day", "week", "month", "year"), vol.Optional("offset"): int, + vol.Optional("first_weekday"): vol.Any(*WEEKDAYS), } ), vol.Exclusive("fixed_period", "period"): vol.Schema( @@ -840,7 +842,12 @@ def resolve_period( start_time += timedelta(days=cal_offset) end_time = start_time + timedelta(days=1) elif calendar_period == "week": - start_time = start_of_day - timedelta(days=start_of_day.weekday()) + first_weekday = WEEKDAYS.index( + period_def["calendar"].get("first_weekday", WEEKDAYS[0]) + ) + start_time = start_of_day - timedelta( + days=(start_of_day.weekday() - first_weekday) % 7 + ) start_time += timedelta(days=cal_offset * 7) end_time = start_time + timedelta(weeks=1) elif calendar_period == "month": diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 2460de994ec52b..46ad05f94bd1f1 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -1742,6 +1742,16 @@ async def assert_stat_during_fixed(client, start_time, end_time, expect): "2022-10-10T07:00:00+00:00", "2022-10-17T07:00:00+00:00", ), + ( + {"period": "week", "first_weekday": "sat"}, + "2022-10-15T07:00:00+00:00", + "2022-10-22T07:00:00+00:00", + ), + ( + {"period": "week", "first_weekday": "fri"}, + "2022-10-21T07:00:00+00:00", + "2022-10-28T07:00:00+00:00", + ), ( {"period": "month"}, "2022-10-01T07:00:00+00:00", From 1cca65b5c5b4fc361346c1292023057bc8b91934 Mon Sep 17 00:00:00 2001 From: Mike Kelly Date: Thu, 4 Sep 2025 02:29:37 -0400 Subject: [PATCH 03/11] Add MCF (1000 Cubic Feet) as an alternate unit of measure for volume (#150015) --- homeassistant/components/energy/sensor.py | 2 ++ homeassistant/components/energy/validate.py | 2 ++ homeassistant/components/number/const.py | 10 ++++--- homeassistant/components/sensor/const.py | 10 ++++--- homeassistant/const.py | 1 + homeassistant/util/unit_conversion.py | 2 ++ homeassistant/util/unit_system.py | 3 +++ tests/components/energy/test_validate.py | 10 ++++--- .../kitchen_sink/snapshots/test_init.ambr | 4 +-- tests/components/sensor/test_recorder.py | 2 +- tests/util/test_unit_conversion.py | 5 ++++ tests/util/test_unit_system.py | 26 ++++++++++++++++++- 12 files changed, 61 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index 1105e6f6b864db..5aa710be19ec2e 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -48,6 +48,7 @@ UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS, UnitOfVolume.LITERS, + UnitOfVolume.MILLE_CUBIC_FEET, *VALID_ENERGY_UNITS, } VALID_VOLUME_UNITS_WATER: set[str] = { @@ -56,6 +57,7 @@ UnitOfVolume.CUBIC_METERS, UnitOfVolume.GALLONS, UnitOfVolume.LITERS, + UnitOfVolume.MILLE_CUBIC_FEET, } _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py index 3590ee9e848766..6c11c2b068c79a 100644 --- a/homeassistant/components/energy/validate.py +++ b/homeassistant/components/energy/validate.py @@ -42,6 +42,7 @@ UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS, UnitOfVolume.LITERS, + UnitOfVolume.MILLE_CUBIC_FEET, ), } GAS_PRICE_UNITS = tuple( @@ -57,6 +58,7 @@ UnitOfVolume.CUBIC_METERS, UnitOfVolume.GALLONS, UnitOfVolume.LITERS, + UnitOfVolume.MILLE_CUBIC_FEET, ), } WATER_PRICE_UNITS = tuple( diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index 22c1170b6b8eee..a9333212fa4566 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -207,7 +207,7 @@ class NumberDeviceClass(StrEnum): Unit of measurement: - SI / metric: `L`, `m³` - - USCS / imperial: `ft³`, `CCF` + - USCS / imperial: `ft³`, `CCF`, `MCF` """ HUMIDITY = "humidity" @@ -398,7 +398,7 @@ class NumberDeviceClass(StrEnum): Unit of measurement: `VOLUME_*` units - SI / metric: `mL`, `L`, `m³` - - USCS / imperial: `ft³`, `CCF`, `fl. oz.`, `gal` (warning: volumes expressed in + - USCS / imperial: `ft³`, `CCF`, `MCF`, `fl. oz.`, `gal` (warning: volumes expressed in USCS/imperial units are currently assumed to be US volumes) """ @@ -410,7 +410,7 @@ class NumberDeviceClass(StrEnum): Unit of measurement: `VOLUME_*` units - SI / metric: `mL`, `L`, `m³` - - USCS / imperial: `ft³`, `CCF`, `fl. oz.`, `gal` (warning: volumes expressed in + - USCS / imperial: `ft³`, `CCF`, `MCF`, `fl. oz.`, `gal` (warning: volumes expressed in USCS/imperial units are currently assumed to be US volumes) """ @@ -427,7 +427,7 @@ class NumberDeviceClass(StrEnum): Unit of measurement: - SI / metric: `m³`, `L` - - USCS / imperial: `ft³`, `CCF`, `gal` (warning: volumes expressed in + - USCS / imperial: `ft³`, `CCF`, `MCF`, `gal` (warning: volumes expressed in USCS/imperial units are currently assumed to be US volumes) """ @@ -493,6 +493,7 @@ class NumberDeviceClass(StrEnum): UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS, UnitOfVolume.LITERS, + UnitOfVolume.MILLE_CUBIC_FEET, }, NumberDeviceClass.HUMIDITY: {PERCENTAGE}, NumberDeviceClass.ILLUMINANCE: {LIGHT_LUX}, @@ -546,6 +547,7 @@ class NumberDeviceClass(StrEnum): UnitOfVolume.CUBIC_METERS, UnitOfVolume.GALLONS, UnitOfVolume.LITERS, + UnitOfVolume.MILLE_CUBIC_FEET, }, NumberDeviceClass.WEIGHT: set(UnitOfMass), NumberDeviceClass.WIND_DIRECTION: {DEGREE}, diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index 94578a6f652739..12d9595d059859 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -240,7 +240,7 @@ class SensorDeviceClass(StrEnum): Unit of measurement: - SI / metric: `L`, `m³` - - USCS / imperial: `ft³`, `CCF` + - USCS / imperial: `ft³`, `CCF`, `MCF` """ HUMIDITY = "humidity" @@ -432,7 +432,7 @@ class SensorDeviceClass(StrEnum): Unit of measurement: `VOLUME_*` units - SI / metric: `mL`, `L`, `m³` - - USCS / imperial: `ft³`, `CCF`, `fl. oz.`, `gal` (warning: volumes expressed in + - USCS / imperial: `ft³`, `CCF`, `MCF`, `fl. oz.`, `gal` (warning: volumes expressed in USCS/imperial units are currently assumed to be US volumes) """ @@ -444,7 +444,7 @@ class SensorDeviceClass(StrEnum): Unit of measurement: `VOLUME_*` units - SI / metric: `mL`, `L`, `m³` - - USCS / imperial: `ft³`, `CCF`, `fl. oz.`, `gal` (warning: volumes expressed in + - USCS / imperial: `ft³`, `CCF`, `MCF`, `fl. oz.`, `gal` (warning: volumes expressed in USCS/imperial units are currently assumed to be US volumes) """ @@ -461,7 +461,7 @@ class SensorDeviceClass(StrEnum): Unit of measurement: - SI / metric: `m³`, `L` - - USCS / imperial: `ft³`, `CCF`, `gal` (warning: volumes expressed in + - USCS / imperial: `ft³`, `CCF`, `MCF`, `gal` (warning: volumes expressed in USCS/imperial units are currently assumed to be US volumes) """ @@ -601,6 +601,7 @@ class SensorStateClass(StrEnum): UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS, UnitOfVolume.LITERS, + UnitOfVolume.MILLE_CUBIC_FEET, }, SensorDeviceClass.HUMIDITY: {PERCENTAGE}, SensorDeviceClass.ILLUMINANCE: {LIGHT_LUX}, @@ -654,6 +655,7 @@ class SensorStateClass(StrEnum): UnitOfVolume.CUBIC_METERS, UnitOfVolume.GALLONS, UnitOfVolume.LITERS, + UnitOfVolume.MILLE_CUBIC_FEET, }, SensorDeviceClass.WEIGHT: set(UnitOfMass), SensorDeviceClass.WIND_DIRECTION: {DEGREE}, diff --git a/homeassistant/const.py b/homeassistant/const.py index 3bd7cc51c7c029..d61945e2ef82cd 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -766,6 +766,7 @@ class UnitOfVolume(StrEnum): CUBIC_FEET = "ft³" CENTUM_CUBIC_FEET = "CCF" + MILLE_CUBIC_FEET = "MCF" CUBIC_METERS = "m³" LITERS = "L" MILLILITERS = "mL" diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index 918b45ff3c9d55..1bd40a12d3d960 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -752,6 +752,7 @@ class VolumeConverter(BaseUnitConverter): UnitOfVolume.CUBIC_METERS: 1, UnitOfVolume.CUBIC_FEET: 1 / _CUBIC_FOOT_TO_CUBIC_METER, UnitOfVolume.CENTUM_CUBIC_FEET: 1 / (100 * _CUBIC_FOOT_TO_CUBIC_METER), + UnitOfVolume.MILLE_CUBIC_FEET: 1 / (1000 * _CUBIC_FOOT_TO_CUBIC_METER), } VALID_UNITS = { UnitOfVolume.LITERS, @@ -761,6 +762,7 @@ class VolumeConverter(BaseUnitConverter): UnitOfVolume.CUBIC_METERS, UnitOfVolume.CUBIC_FEET, UnitOfVolume.CENTUM_CUBIC_FEET, + UnitOfVolume.MILLE_CUBIC_FEET, } diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index 31f74377a162ea..934cd6d4b693c5 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -281,6 +281,7 @@ def _deprecated_unit_system(value: str) -> str: # Convert non-metric volumes of gas meters ("gas", UnitOfVolume.CENTUM_CUBIC_FEET): UnitOfVolume.CUBIC_METERS, ("gas", UnitOfVolume.CUBIC_FEET): UnitOfVolume.CUBIC_METERS, + ("gas", UnitOfVolume.MILLE_CUBIC_FEET): UnitOfVolume.CUBIC_METERS, # Convert non-metric precipitation ("precipitation", UnitOfLength.INCHES): UnitOfLength.MILLIMETERS, # Convert non-metric precipitation intensity @@ -312,10 +313,12 @@ def _deprecated_unit_system(value: str) -> str: ("volume", UnitOfVolume.CUBIC_FEET): UnitOfVolume.CUBIC_METERS, ("volume", UnitOfVolume.FLUID_OUNCES): UnitOfVolume.MILLILITERS, ("volume", UnitOfVolume.GALLONS): UnitOfVolume.LITERS, + ("volume", UnitOfVolume.MILLE_CUBIC_FEET): UnitOfVolume.CUBIC_METERS, # Convert non-metric volumes of water meters ("water", UnitOfVolume.CENTUM_CUBIC_FEET): UnitOfVolume.CUBIC_METERS, ("water", UnitOfVolume.CUBIC_FEET): UnitOfVolume.CUBIC_METERS, ("water", UnitOfVolume.GALLONS): UnitOfVolume.LITERS, + ("water", UnitOfVolume.MILLE_CUBIC_FEET): UnitOfVolume.CUBIC_METERS, # Convert wind speeds except knots to km/h **{ ("wind_speed", unit): UnitOfSpeed.KILOMETERS_PER_HOUR diff --git a/tests/components/energy/test_validate.py b/tests/components/energy/test_validate.py index 9e7a2151b04481..9addf6c10015b1 100644 --- a/tests/components/energy/test_validate.py +++ b/tests/components/energy/test_validate.py @@ -850,7 +850,7 @@ async def test_validation_gas( "affected_entities": {("sensor.gas_consumption_1", "beers")}, "translation_placeholders": { "energy_units": ENERGY_UNITS_STRING, - "gas_units": "CCF, ft³, m³, L", + "gas_units": "CCF, ft³, m³, L, MCF", }, }, { @@ -879,7 +879,7 @@ async def test_validation_gas( "affected_entities": {("sensor.gas_price_2", "EUR/invalid")}, "translation_placeholders": { "price_units": ( - f"{ENERGY_PRICE_UNITS_STRING}, EUR/CCF, EUR/ft³, EUR/m³, EUR/L" + f"{ENERGY_PRICE_UNITS_STRING}, EUR/CCF, EUR/ft³, EUR/m³, EUR/L, EUR/MCF" ) }, }, @@ -1060,7 +1060,9 @@ async def test_validation_water( { "type": "entity_unexpected_unit_water", "affected_entities": {("sensor.water_consumption_1", "beers")}, - "translation_placeholders": {"water_units": "CCF, ft³, m³, gal, L"}, + "translation_placeholders": { + "water_units": "CCF, ft³, m³, gal, L, MCF" + }, }, { "type": "recorder_untracked", @@ -1087,7 +1089,7 @@ async def test_validation_water( "type": "entity_unexpected_unit_water_price", "affected_entities": {("sensor.water_price_2", "EUR/invalid")}, "translation_placeholders": { - "price_units": "EUR/CCF, EUR/ft³, EUR/m³, EUR/gal, EUR/L" + "price_units": "EUR/CCF, EUR/ft³, EUR/m³, EUR/gal, EUR/L, EUR/MCF" }, }, ], diff --git a/tests/components/kitchen_sink/snapshots/test_init.ambr b/tests/components/kitchen_sink/snapshots/test_init.ambr index fe22f19fb7a30b..c7161a3d284695 100644 --- a/tests/components/kitchen_sink/snapshots/test_init.ambr +++ b/tests/components/kitchen_sink/snapshots/test_init.ambr @@ -7,7 +7,7 @@ 'metadata_unit': 'm³', 'state_unit': 'W', 'statistic_id': 'sensor.statistics_issues_issue_1', - 'supported_unit': 'CCF, L, fl. oz., ft³, gal, mL, m³', + 'supported_unit': 'CCF, L, MCF, fl. oz., ft³, gal, mL, m³', }), 'type': 'units_changed', }), @@ -35,7 +35,7 @@ 'metadata_unit': 'm³', 'state_unit': 'W', 'statistic_id': 'sensor.statistics_issues_issue_3', - 'supported_unit': 'CCF, L, fl. oz., ft³, gal, mL, m³', + 'supported_unit': 'CCF, L, MCF, fl. oz., ft³, gal, mL, m³', }), 'type': 'units_changed', }), diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 8b6d55cb9a9459..09f2480891ee4c 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -5824,7 +5824,7 @@ async def test_validate_statistics_unit_change_equivalent_units( @pytest.mark.parametrize( ("attributes", "unit1", "unit2", "supported_unit"), [ - (NONE_SENSOR_ATTRIBUTES, "m³", "m3", "CCF, L, fl. oz., ft³, gal, mL, m³"), + (NONE_SENSOR_ATTRIBUTES, "m³", "m3", "CCF, L, MCF, fl. oz., ft³, gal, mL, m³"), (NONE_SENSOR_ATTRIBUTES, "\u03bcV", "\u00b5V", "MV, V, kV, mV, \u03bcV"), (NONE_SENSOR_ATTRIBUTES, "\u03bcS/cm", "\u00b5S/cm", "S/cm, mS/cm, \u03bcS/cm"), ( diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index 476cb667d90279..3fe0078aabfa3b 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -879,6 +879,11 @@ (5, UnitOfVolume.CENTUM_CUBIC_FEET, 478753.24, UnitOfVolume.FLUID_OUNCES), (5, UnitOfVolume.CENTUM_CUBIC_FEET, 3740.26, UnitOfVolume.GALLONS), (5, UnitOfVolume.CENTUM_CUBIC_FEET, 14158.42, UnitOfVolume.LITERS), + (5, UnitOfVolume.MILLE_CUBIC_FEET, 5000, UnitOfVolume.CUBIC_FEET), + (5, UnitOfVolume.MILLE_CUBIC_FEET, 141.5842, UnitOfVolume.CUBIC_METERS), + (5, UnitOfVolume.MILLE_CUBIC_FEET, 4787532.4, UnitOfVolume.FLUID_OUNCES), + (5, UnitOfVolume.MILLE_CUBIC_FEET, 37402.6, UnitOfVolume.GALLONS), + (5, UnitOfVolume.MILLE_CUBIC_FEET, 141584.2, UnitOfVolume.LITERS), ], VolumeFlowRateConverter: [ ( diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py index 87a9729700ec0c..e8da55358a3b94 100644 --- a/tests/util/test_unit_system.py +++ b/tests/util/test_unit_system.py @@ -433,6 +433,11 @@ def test_get_unit_system_invalid(key: str) -> None: UnitOfVolume.CENTUM_CUBIC_FEET, UnitOfVolume.CUBIC_METERS, ), + ( + SensorDeviceClass.GAS, + UnitOfVolume.MILLE_CUBIC_FEET, + UnitOfVolume.CUBIC_METERS, + ), (SensorDeviceClass.GAS, UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS), (SensorDeviceClass.GAS, UnitOfVolume.LITERS, None), (SensorDeviceClass.GAS, UnitOfVolume.CUBIC_METERS, None), @@ -510,6 +515,11 @@ def test_get_unit_system_invalid(key: str) -> None: UnitOfVolume.CENTUM_CUBIC_FEET, UnitOfVolume.CUBIC_METERS, ), + ( + SensorDeviceClass.VOLUME, + UnitOfVolume.MILLE_CUBIC_FEET, + UnitOfVolume.CUBIC_METERS, + ), (SensorDeviceClass.VOLUME, UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS), (SensorDeviceClass.VOLUME, UnitOfVolume.FLUID_OUNCES, UnitOfVolume.MILLILITERS), (SensorDeviceClass.VOLUME, UnitOfVolume.GALLONS, UnitOfVolume.LITERS), @@ -523,6 +533,11 @@ def test_get_unit_system_invalid(key: str) -> None: UnitOfVolume.CENTUM_CUBIC_FEET, UnitOfVolume.CUBIC_METERS, ), + ( + SensorDeviceClass.WATER, + UnitOfVolume.MILLE_CUBIC_FEET, + UnitOfVolume.CUBIC_METERS, + ), (SensorDeviceClass.WATER, UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS), (SensorDeviceClass.WATER, UnitOfVolume.GALLONS, UnitOfVolume.LITERS), (SensorDeviceClass.WATER, UnitOfVolume.CUBIC_METERS, None), @@ -690,6 +705,7 @@ def test_metric_converted_units(device_class: SensorDeviceClass) -> None: (SensorDeviceClass.DISTANCE, "very_long", None), # Test gas meter conversion (SensorDeviceClass.GAS, UnitOfVolume.CENTUM_CUBIC_FEET, None), + (SensorDeviceClass.GAS, UnitOfVolume.MILLE_CUBIC_FEET, None), (SensorDeviceClass.GAS, UnitOfVolume.CUBIC_METERS, UnitOfVolume.CUBIC_FEET), (SensorDeviceClass.GAS, UnitOfVolume.LITERS, UnitOfVolume.CUBIC_FEET), (SensorDeviceClass.GAS, UnitOfVolume.CUBIC_FEET, None), @@ -770,6 +786,7 @@ def test_metric_converted_units(device_class: SensorDeviceClass) -> None: (SensorDeviceClass.VOLUME, UnitOfVolume.LITERS, UnitOfVolume.GALLONS), (SensorDeviceClass.VOLUME, UnitOfVolume.MILLILITERS, UnitOfVolume.FLUID_OUNCES), (SensorDeviceClass.VOLUME, UnitOfVolume.CENTUM_CUBIC_FEET, None), + (SensorDeviceClass.VOLUME, UnitOfVolume.MILLE_CUBIC_FEET, None), (SensorDeviceClass.VOLUME, UnitOfVolume.CUBIC_FEET, None), (SensorDeviceClass.VOLUME, UnitOfVolume.FLUID_OUNCES, None), (SensorDeviceClass.VOLUME, UnitOfVolume.GALLONS, None), @@ -778,6 +795,7 @@ def test_metric_converted_units(device_class: SensorDeviceClass) -> None: (SensorDeviceClass.WATER, UnitOfVolume.CUBIC_METERS, UnitOfVolume.CUBIC_FEET), (SensorDeviceClass.WATER, UnitOfVolume.LITERS, UnitOfVolume.GALLONS), (SensorDeviceClass.WATER, UnitOfVolume.CENTUM_CUBIC_FEET, None), + (SensorDeviceClass.WATER, UnitOfVolume.MILLE_CUBIC_FEET, None), (SensorDeviceClass.WATER, UnitOfVolume.CUBIC_FEET, None), (SensorDeviceClass.WATER, UnitOfVolume.GALLONS, None), (SensorDeviceClass.WATER, "very_much", None), @@ -828,7 +846,11 @@ def test_get_us_converted_unit( UnitOfLength.MILES, UnitOfLength.YARDS, ), - SensorDeviceClass.GAS: (UnitOfVolume.CENTUM_CUBIC_FEET, UnitOfVolume.CUBIC_FEET), + SensorDeviceClass.GAS: ( + UnitOfVolume.CENTUM_CUBIC_FEET, + UnitOfVolume.MILLE_CUBIC_FEET, + UnitOfVolume.CUBIC_FEET, + ), SensorDeviceClass.PRECIPITATION: (UnitOfLength.INCHES,), SensorDeviceClass.PRECIPITATION_INTENSITY: ( UnitOfVolumetricFlux.INCHES_PER_DAY, @@ -846,12 +868,14 @@ def test_get_us_converted_unit( ), SensorDeviceClass.VOLUME: ( UnitOfVolume.CENTUM_CUBIC_FEET, + UnitOfVolume.MILLE_CUBIC_FEET, UnitOfVolume.CUBIC_FEET, UnitOfVolume.FLUID_OUNCES, UnitOfVolume.GALLONS, ), SensorDeviceClass.WATER: ( UnitOfVolume.CENTUM_CUBIC_FEET, + UnitOfVolume.MILLE_CUBIC_FEET, UnitOfVolume.CUBIC_FEET, UnitOfVolume.GALLONS, ), From f28251bc76d0c6034dd9012bf260c8fd1a121b9d Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Thu, 4 Sep 2025 09:20:15 +0200 Subject: [PATCH 04/11] Small fixes of user-facing strings in `fritz` (#151663) --- homeassistant/components/fritz/strings.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fritz/strings.json b/homeassistant/components/fritz/strings.json index 5d5aba2af60363..75ce6800aab255 100644 --- a/homeassistant/components/fritz/strings.json +++ b/homeassistant/components/fritz/strings.json @@ -28,7 +28,7 @@ } }, "reauth_confirm": { - "title": "Updating FRITZ!Box Tools - credentials", + "title": "FRITZ!Box Tools - Update credentials", "description": "Update FRITZ!Box Tools credentials for: {host}.\n\nFRITZ!Box Tools is unable to log in to your FRITZ!Box.", "data": { "username": "[%key:common::config_flow::data::username%]", @@ -40,7 +40,7 @@ } }, "reconfigure": { - "title": "Updating FRITZ!Box Tools - configuration", + "title": "FRITZ!Box Tools - Update configuration", "description": "Update FRITZ!Box Tools configuration for: {host}.", "data": { "host": "[%key:common::config_flow::data::host%]", @@ -183,7 +183,7 @@ "description": "Sets a new password for the guest Wi-Fi. The password must be between 8 and 63 characters long. If no additional parameter is set, the password will be auto-generated with a length of 12 characters.", "fields": { "device_id": { - "name": "FRITZ!Box Device", + "name": "FRITZ!Box device", "description": "Select the FRITZ!Box to configure." }, "password": { @@ -192,7 +192,7 @@ }, "length": { "name": "Password length", - "description": "Length of the new password. The password will be auto-generated, if no password is set." + "description": "Length of the new password. It will be auto-generated if no password is set." } } } From ab5ef3674f40da7ca556efe07b4da35a8daaaf87 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 09:22:22 +0200 Subject: [PATCH 05/11] Bump pypa/gh-action-pypi-publish from 1.12.4 to 1.13.0 (#151661) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index e305e7c76e692d..87b5dd1ae77f55 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -480,7 +480,7 @@ jobs: python -m build - name: Upload package to PyPI - uses: pypa/gh-action-pypi-publish@v1.12.4 + uses: pypa/gh-action-pypi-publish@v1.13.0 with: skip-existing: true From 2a458dcec91975bc4b50b89abaf036d1e74f87b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 09:22:28 +0200 Subject: [PATCH 06/11] Bump actions/setup-python from 5.6.0 to 6.0.0 (#151662) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 6 +++--- .github/workflows/ci.yaml | 32 +++++++++++++++--------------- .github/workflows/translations.yml | 2 +- .github/workflows/wheels.yml | 2 +- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 87b5dd1ae77f55..63cafce6c731f4 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -32,7 +32,7 @@ jobs: fetch-depth: 0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -116,7 +116,7 @@ jobs: - name: Set up Python ${{ env.DEFAULT_PYTHON }} if: needs.init.outputs.channel == 'dev' - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -457,7 +457,7 @@ jobs: uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0a0e2864a649cb..ec8c9c7b19e648 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -249,7 +249,7 @@ jobs: uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true @@ -294,7 +294,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -334,7 +334,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -374,7 +374,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -484,7 +484,7 @@ jobs: uses: actions/checkout@v5.0.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -587,7 +587,7 @@ jobs: uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true @@ -620,7 +620,7 @@ jobs: uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true @@ -677,7 +677,7 @@ jobs: uses: actions/checkout@v5.0.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -720,7 +720,7 @@ jobs: uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true @@ -767,7 +767,7 @@ jobs: uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true @@ -812,7 +812,7 @@ jobs: uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true @@ -889,7 +889,7 @@ jobs: uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true @@ -950,7 +950,7 @@ jobs: uses: actions/checkout@v5.0.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -1083,7 +1083,7 @@ jobs: uses: actions/checkout@v5.0.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -1225,7 +1225,7 @@ jobs: uses: actions/checkout@v5.0.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -1384,7 +1384,7 @@ jobs: uses: actions/checkout@v5.0.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ matrix.python-version }} check-latest: true diff --git a/.github/workflows/translations.yml b/.github/workflows/translations.yml index 004b552cab3c8f..e0ffe2933e009a 100644 --- a/.github/workflows/translations.yml +++ b/.github/workflows/translations.yml @@ -22,7 +22,7 @@ jobs: uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 883cc688cf5fa5..7ac7c239816412 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -36,7 +36,7 @@ jobs: - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true From c2290d6edb55101eb74164f1c3337c75f85fd298 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Thu, 4 Sep 2025 04:30:23 -0300 Subject: [PATCH 07/11] Fix WebSocket proxy for add-ons not forwarding ping/pong frame data (#151654) --- homeassistant/components/hassio/ingress.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index e1f96b76bcb1a3..2938de927210d9 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -303,9 +303,9 @@ async def _websocket_forward( elif msg.type is aiohttp.WSMsgType.BINARY: await ws_to.send_bytes(msg.data) elif msg.type is aiohttp.WSMsgType.PING: - await ws_to.ping() + await ws_to.ping(msg.data) elif msg.type is aiohttp.WSMsgType.PONG: - await ws_to.pong() + await ws_to.pong(msg.data) elif ws_to.closed: await ws_to.close(code=ws_to.close_code, message=msg.extra) # type: ignore[arg-type] except RuntimeError: From 3bc772a19628423936a2cb55cb5000357bb88231 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Thu, 4 Sep 2025 03:33:43 -0400 Subject: [PATCH 08/11] Fix Sonos Dialog Select type conversion (#151649) --- homeassistant/components/sonos/select.py | 11 +++++++++-- homeassistant/components/sonos/speaker.py | 7 ++++++- tests/components/sonos/test_select.py | 8 ++++---- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sonos/select.py b/homeassistant/components/sonos/select.py index 052a1d87967639..0a56e37e75c0f0 100644 --- a/homeassistant/components/sonos/select.py +++ b/homeassistant/components/sonos/select.py @@ -61,8 +61,15 @@ def available_soco_attributes( if ( state := getattr(speaker.soco, select_data.soco_attribute, None) ) is not None: - setattr(speaker, select_data.speaker_attribute, state) - features.append(select_data) + try: + setattr(speaker, select_data.speaker_attribute, int(state)) + features.append(select_data) + except ValueError: + _LOGGER.error( + "Invalid value for %s %s", + select_data.speaker_attribute, + state, + ) return features async def _async_create_entities(speaker: SonosSpeaker) -> None: diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 427f02f0479899..acf1b08cd36ed2 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -599,7 +599,12 @@ def async_update_volume(self, event: SonosEvent) -> None: for enum_var in (ATTR_DIALOG_LEVEL,): if enum_var in variables: - setattr(self, f"{enum_var}_enum", variables[enum_var]) + try: + setattr(self, f"{enum_var}_enum", int(variables[enum_var])) + except ValueError: + _LOGGER.error( + "Invalid value for %s %s", enum_var, variables[enum_var] + ) self.async_write_entity_states() diff --git a/tests/components/sonos/test_select.py b/tests/components/sonos/test_select.py index ada48de21f3960..dbbf28a52d74a2 100644 --- a/tests/components/sonos/test_select.py +++ b/tests/components/sonos/test_select.py @@ -38,9 +38,9 @@ async def platform_binary_sensor_fixture(): [ (0, "off"), (1, "low"), - (2, "medium"), - (3, "high"), - (4, "max"), + ("2", "medium"), + ("3", "high"), + ("4", "max"), ], ) async def test_select_dialog_level( @@ -49,7 +49,7 @@ async def test_select_dialog_level( soco, entity_registry: er.EntityRegistry, speaker_info: dict[str, str], - level: int, + level: int | str, result: str, ) -> None: """Test dialog level select entity.""" From 86e7f3713fb66177357869162e2752946e6b0bf0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 10:11:50 +0200 Subject: [PATCH 09/11] Bump actions/stale from 9.1.0 to 10.0.0 (#151660) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/stale.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 11c87266525c01..f0e2572fa54140 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -17,7 +17,7 @@ jobs: # - No PRs marked as no-stale # - No issues (-1) - name: 60 days stale PRs policy - uses: actions/stale@v9.1.0 + uses: actions/stale@v10.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 60 @@ -57,7 +57,7 @@ jobs: # - No issues marked as no-stale or help-wanted # - No PRs (-1) - name: 90 days stale issues - uses: actions/stale@v9.1.0 + uses: actions/stale@v10.0.0 with: repo-token: ${{ steps.token.outputs.token }} days-before-stale: 90 @@ -87,7 +87,7 @@ jobs: # - No Issues marked as no-stale or help-wanted # - No PRs (-1) - name: Needs more information stale issues policy - uses: actions/stale@v9.1.0 + uses: actions/stale@v10.0.0 with: repo-token: ${{ steps.token.outputs.token }} only-labels: "needs-more-information" From 8d945d89de185b795c5684aae6f08eb328c9d6f0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Sep 2025 10:37:25 +0200 Subject: [PATCH 10/11] Bump tuya-device-sharing-sdk to 0.2.3 (#151659) --- homeassistant/components/tuya/__init__.py | 16 ++++++++++++---- homeassistant/components/tuya/entity.py | 4 +++- homeassistant/components/tuya/event.py | 4 +++- homeassistant/components/tuya/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 21 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index fc408531a386ff..229c890cecb96b 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -227,21 +227,29 @@ def __init__( self.hass = hass self.manager = manager - def update_device( - self, device: CustomerDevice, updated_status_properties: list[str] | None + # pylint disable can be removed when issue fixed in library + # https://github.com/tuya/tuya-device-sharing-sdk/pull/35 + def update_device( # pylint: disable=arguments-renamed + self, + device: CustomerDevice, + updated_status_properties: list[str] | None = None, + dp_timestamps: dict | None = None, ) -> None: - """Update device status.""" + """Update device status with optional DP timestamps.""" LOGGER.debug( - "Received update for device %s (online: %s): %s (updated properties: %s)", + "Received update for device %s (online: %s): %s" + " (updated properties: %s, dp_timestamps: %s)", device.id, device.online, device.status, updated_status_properties, + dp_timestamps, ) dispatcher_send( self.hass, f"{TUYA_HA_SIGNAL_UPDATE_ENTITY}_{device.id}", updated_status_properties, + dp_timestamps, ) def add_device(self, device: CustomerDevice) -> None: diff --git a/homeassistant/components/tuya/entity.py b/homeassistant/components/tuya/entity.py index 0ae0f793afdf77..7d51a006877b49 100644 --- a/homeassistant/components/tuya/entity.py +++ b/homeassistant/components/tuya/entity.py @@ -158,7 +158,9 @@ async def async_added_to_hass(self) -> None: ) async def _handle_state_update( - self, updated_status_properties: list[str] | None + self, + updated_status_properties: list[str] | None, + dp_timestamps: dict | None = None, ) -> None: self.async_write_ha_state() diff --git a/homeassistant/components/tuya/event.py b/homeassistant/components/tuya/event.py index 09ab8e8f544f92..0c07844ffba208 100644 --- a/homeassistant/components/tuya/event.py +++ b/homeassistant/components/tuya/event.py @@ -134,7 +134,9 @@ def __init__( self._attr_event_types: list[str] = dpcode.range async def _handle_state_update( - self, updated_status_properties: list[str] | None + self, + updated_status_properties: list[str] | None, + dp_timestamps: dict | None = None, ) -> None: if ( updated_status_properties is None diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index 96ee50a38c902a..b130cb4c0e290b 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -43,5 +43,5 @@ "integration_type": "hub", "iot_class": "cloud_push", "loggers": ["tuya_iot"], - "requirements": ["tuya-device-sharing-sdk==0.2.1"] + "requirements": ["tuya-device-sharing-sdk==0.2.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index d89119f0697d1a..640002581bebd2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2991,7 +2991,7 @@ ttls==1.8.3 ttn_client==1.2.0 # homeassistant.components.tuya -tuya-device-sharing-sdk==0.2.1 +tuya-device-sharing-sdk==0.2.3 # homeassistant.components.twentemilieu twentemilieu==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 49e0337a03627a..841b840f236d7e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2465,7 +2465,7 @@ ttls==1.8.3 ttn_client==1.2.0 # homeassistant.components.tuya -tuya-device-sharing-sdk==0.2.1 +tuya-device-sharing-sdk==0.2.3 # homeassistant.components.twentemilieu twentemilieu==2.2.1 From eae1fe4a56092876089afa07986a03b1167e5da1 Mon Sep 17 00:00:00 2001 From: Kevin McCormack Date: Thu, 4 Sep 2025 05:20:16 -0400 Subject: [PATCH 11/11] Add strict typing, shared constants, and fix OPNsense name casing (#151599) --- .strict-typing | 1 + homeassistant/components/opnsense/__init__.py | 25 +++++----- homeassistant/components/opnsense/const.py | 8 +++ .../components/opnsense/device_tracker.py | 50 +++++++++++-------- .../components/opnsense/manifest.json | 2 +- homeassistant/generated/integrations.json | 2 +- mypy.ini | 10 ++++ 7 files changed, 63 insertions(+), 35 deletions(-) create mode 100644 homeassistant/components/opnsense/const.py diff --git a/.strict-typing b/.strict-typing index 452a6f647a7453..ce06d00c697928 100644 --- a/.strict-typing +++ b/.strict-typing @@ -383,6 +383,7 @@ homeassistant.components.openai_conversation.* homeassistant.components.openexchangerates.* homeassistant.components.opensky.* homeassistant.components.openuv.* +homeassistant.components.opnsense.* homeassistant.components.opower.* homeassistant.components.oralb.* homeassistant.components.otbr.* diff --git a/homeassistant/components/opnsense/__init__.py b/homeassistant/components/opnsense/__init__.py index 66f35a51b87cf5..bc085dbfa4d92e 100644 --- a/homeassistant/components/opnsense/__init__.py +++ b/homeassistant/components/opnsense/__init__.py @@ -1,4 +1,4 @@ -"""Support for OPNSense Routers.""" +"""Support for OPNsense Routers.""" import logging @@ -12,14 +12,15 @@ from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.typing import ConfigType -_LOGGER = logging.getLogger(__name__) - -CONF_API_SECRET = "api_secret" -CONF_TRACKER_INTERFACE = "tracker_interfaces" - -DOMAIN = "opnsense" +from .const import ( + CONF_API_SECRET, + CONF_INTERFACE_CLIENT, + CONF_TRACKER_INTERFACES, + DOMAIN, + OPNSENSE_DATA, +) -OPNSENSE_DATA = DOMAIN +_LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( { @@ -29,7 +30,7 @@ vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_API_SECRET): cv.string, vol.Optional(CONF_VERIFY_SSL, default=False): cv.boolean, - vol.Optional(CONF_TRACKER_INTERFACE, default=[]): vol.All( + vol.Optional(CONF_TRACKER_INTERFACES, default=[]): vol.All( cv.ensure_list, [cv.string] ), } @@ -47,7 +48,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: api_key = conf[CONF_API_KEY] api_secret = conf[CONF_API_SECRET] verify_ssl = conf[CONF_VERIFY_SSL] - tracker_interfaces = conf[CONF_TRACKER_INTERFACE] + tracker_interfaces = conf[CONF_TRACKER_INTERFACES] interfaces_client = diagnostics.InterfaceClient( api_key, api_secret, url, verify_ssl, timeout=20 @@ -72,8 +73,8 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: return False hass.data[OPNSENSE_DATA] = { - "interfaces": interfaces_client, - CONF_TRACKER_INTERFACE: tracker_interfaces, + CONF_INTERFACE_CLIENT: interfaces_client, + CONF_TRACKER_INTERFACES: tracker_interfaces, } load_platform(hass, Platform.DEVICE_TRACKER, DOMAIN, tracker_interfaces, config) diff --git a/homeassistant/components/opnsense/const.py b/homeassistant/components/opnsense/const.py new file mode 100644 index 00000000000000..62ab16701f49fc --- /dev/null +++ b/homeassistant/components/opnsense/const.py @@ -0,0 +1,8 @@ +"""Constants for OPNsense component.""" + +DOMAIN = "opnsense" +OPNSENSE_DATA = DOMAIN + +CONF_API_SECRET = "api_secret" +CONF_INTERFACE_CLIENT = "interface_client" +CONF_TRACKER_INTERFACES = "tracker_interfaces" diff --git a/homeassistant/components/opnsense/device_tracker.py b/homeassistant/components/opnsense/device_tracker.py index 6357ce38e1dd4a..5f6d8d2d43638f 100644 --- a/homeassistant/components/opnsense/device_tracker.py +++ b/homeassistant/components/opnsense/device_tracker.py @@ -1,34 +1,41 @@ -"""Device tracker support for OPNSense routers.""" +"""Device tracker support for OPNsense routers.""" -from __future__ import annotations +from typing import Any, NewType + +from pyopnsense import diagnostics from homeassistant.components.device_tracker import DeviceScanner from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType -from . import CONF_TRACKER_INTERFACE, OPNSENSE_DATA +from .const import CONF_INTERFACE_CLIENT, CONF_TRACKER_INTERFACES, OPNSENSE_DATA + +DeviceDetails = NewType("DeviceDetails", dict[str, Any]) +DeviceDetailsByMAC = NewType("DeviceDetailsByMAC", dict[str, DeviceDetails]) async def async_get_scanner( hass: HomeAssistant, config: ConfigType -) -> OPNSenseDeviceScanner: - """Configure the OPNSense device_tracker.""" - interface_client = hass.data[OPNSENSE_DATA]["interfaces"] - return OPNSenseDeviceScanner( - interface_client, hass.data[OPNSENSE_DATA][CONF_TRACKER_INTERFACE] +) -> DeviceScanner | None: + """Configure the OPNsense device_tracker.""" + return OPNsenseDeviceScanner( + hass.data[OPNSENSE_DATA][CONF_INTERFACE_CLIENT], + hass.data[OPNSENSE_DATA][CONF_TRACKER_INTERFACES], ) -class OPNSenseDeviceScanner(DeviceScanner): - """Class which queries a router running OPNsense.""" +class OPNsenseDeviceScanner(DeviceScanner): + """This class queries a router running OPNsense.""" - def __init__(self, client, interfaces): + def __init__( + self, client: diagnostics.InterfaceClient, interfaces: list[str] + ) -> None: """Initialize the scanner.""" - self.last_results = {} + self.last_results: dict[str, Any] = {} self.client = client self.interfaces = interfaces - def _get_mac_addrs(self, devices): + def _get_mac_addrs(self, devices: list[DeviceDetails]) -> DeviceDetailsByMAC | dict: """Create dict with mac address keys from list of devices.""" out_devices = {} for device in devices: @@ -36,30 +43,31 @@ def _get_mac_addrs(self, devices): out_devices[device["mac"]] = device return out_devices - def scan_devices(self): + def scan_devices(self) -> list[str]: """Scan for new devices and return a list with found device IDs.""" self.update_info() return list(self.last_results) - def get_device_name(self, device): + def get_device_name(self, device: str) -> str | None: """Return the name of the given device or None if we don't know.""" if device not in self.last_results: return None return self.last_results[device].get("hostname") or None - def update_info(self): - """Ensure the information from the OPNSense router is up to date. + def update_info(self) -> bool: + """Ensure the information from the OPNsense router is up to date. Return boolean if scanning successful. """ - devices = self.client.get_arp() self.last_results = self._get_mac_addrs(devices) + return True - def get_extra_attributes(self, device): + def get_extra_attributes(self, device: str) -> dict[Any, Any]: """Return the extra attrs of the given device.""" if device not in self.last_results: - return None - if not (mfg := self.last_results[device].get("manufacturer")): + return {} + mfg = self.last_results[device].get("manufacturer") + if not mfg: return {} return {"manufacturer": mfg} diff --git a/homeassistant/components/opnsense/manifest.json b/homeassistant/components/opnsense/manifest.json index 4dd82216f1af51..0a9aecbde2560c 100644 --- a/homeassistant/components/opnsense/manifest.json +++ b/homeassistant/components/opnsense/manifest.json @@ -1,6 +1,6 @@ { "domain": "opnsense", - "name": "OPNSense", + "name": "OPNsense", "codeowners": ["@mtreinish"], "documentation": "https://www.home-assistant.io/integrations/opnsense", "iot_class": "local_polling", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 0f7e6e2716c8d9..4e243fb686f359 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4736,7 +4736,7 @@ } }, "opnsense": { - "name": "OPNSense", + "name": "OPNsense", "integration_type": "hub", "config_flow": false, "iot_class": "local_polling" diff --git a/mypy.ini b/mypy.ini index db883045f8519d..41ab0f88a10ab5 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3586,6 +3586,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.opnsense.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.opower.*] check_untyped_defs = true disallow_incomplete_defs = true