From c9a6b1fd4574db81969a65290269401b6f25baef Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 30 Jun 2025 09:39:02 +0200 Subject: [PATCH 1/7] Bump reolink_aio to 0.14.2 (#147797) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 04996689bf7272..c422af292b9f6b 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -19,5 +19,5 @@ "iot_class": "local_push", "loggers": ["reolink_aio"], "quality_scale": "platinum", - "requirements": ["reolink-aio==0.14.1"] + "requirements": ["reolink-aio==0.14.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9aa22cdd315c81..3c63782bacc9ba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2656,7 +2656,7 @@ renault-api==0.3.1 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.14.1 +reolink-aio==0.14.2 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cc8cae22ef74d6..a82c6fad43729e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2202,7 +2202,7 @@ renault-api==0.3.1 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.14.1 +reolink-aio==0.14.2 # homeassistant.components.rflink rflink==0.0.67 From 97c1e21a69c054cbb18ebdcfd9ee2d6f087955c7 Mon Sep 17 00:00:00 2001 From: Alexandre CUER Date: Mon, 30 Jun 2025 10:05:07 +0200 Subject: [PATCH 2/7] Add possibility to synchronize automatically all available feeds in emoncms (#128122) * Add checkbox in options to sync all feeds once * Add sync mode selector in async_step_user Remove checkbox in options * Correct use of SYNC_MODE & SYNC_MODE_AUTO in tests * Use dropdown for mode selection * rmv_unused_const * Add separate tests + use SelectSelector --- .../components/emoncms/config_flow.py | 30 +++++++- homeassistant/components/emoncms/const.py | 3 + homeassistant/components/emoncms/strings.json | 11 ++- tests/components/emoncms/test_config_flow.py | 71 ++++++++++++++----- 4 files changed, 96 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/emoncms/config_flow.py b/homeassistant/components/emoncms/config_flow.py index 8b3067b2cf4161..c34aa1b629b0aa 100644 --- a/homeassistant/components/emoncms/config_flow.py +++ b/homeassistant/components/emoncms/config_flow.py @@ -16,7 +16,12 @@ from homeassistant.const import CONF_API_KEY, CONF_URL from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.selector import selector +from homeassistant.helpers.selector import ( + SelectSelector, + SelectSelectorConfig, + SelectSelectorMode, + selector, +) from .const import ( CONF_MESSAGE, @@ -26,6 +31,9 @@ FEED_ID, FEED_NAME, FEED_TAG, + SYNC_MODE, + SYNC_MODE_AUTO, + SYNC_MODE_MANUAL, ) @@ -102,6 +110,17 @@ async def async_step_user( "mode": "dropdown", "multiple": True, } + if user_input.get(SYNC_MODE) == SYNC_MODE_AUTO: + return self.async_create_entry( + title=sensor_name(self.url), + data={ + CONF_URL: self.url, + CONF_API_KEY: self.api_key, + CONF_ONLY_INCLUDE_FEEDID: [ + feed[FEED_ID] for feed in result[CONF_MESSAGE] + ], + }, + ) return await self.async_step_choose_feeds() return self.async_show_form( step_id="user", @@ -110,6 +129,15 @@ async def async_step_user( { vol.Required(CONF_URL): str, vol.Required(CONF_API_KEY): str, + vol.Required( + SYNC_MODE, default=SYNC_MODE_MANUAL + ): SelectSelector( + SelectSelectorConfig( + options=[SYNC_MODE_MANUAL, SYNC_MODE_AUTO], + mode=SelectSelectorMode.DROPDOWN, + translation_key=SYNC_MODE, + ) + ), } ), user_input, diff --git a/homeassistant/components/emoncms/const.py b/homeassistant/components/emoncms/const.py index c53f7cc8a9f5ba..a3b4629493f738 100644 --- a/homeassistant/components/emoncms/const.py +++ b/homeassistant/components/emoncms/const.py @@ -14,6 +14,9 @@ FEED_ID = "id" FEED_NAME = "name" FEED_TAG = "tag" +SYNC_MODE = "sync_mode" +SYNC_MODE_AUTO = "auto" +SYNC_MODE_MANUAL = "manual" LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/emoncms/strings.json b/homeassistant/components/emoncms/strings.json index 451a3fb88e53cc..3efb0720eab760 100644 --- a/homeassistant/components/emoncms/strings.json +++ b/homeassistant/components/emoncms/strings.json @@ -7,7 +7,8 @@ "user": { "data": { "url": "[%key:common::config_flow::data::url%]", - "api_key": "[%key:common::config_flow::data::api_key%]" + "api_key": "[%key:common::config_flow::data::api_key%]", + "sync_mode": "Synchronization mode" }, "data_description": { "url": "Server URL starting with the protocol (http or https)", @@ -24,6 +25,14 @@ "already_configured": "This server is already configured" } }, + "selector": { + "sync_mode": { + "options": { + "auto": "Synchronize all available Feeds", + "manual": "Select which Feeds to synchronize" + } + } + }, "entity": { "sensor": { "energy": { diff --git a/tests/components/emoncms/test_config_flow.py b/tests/components/emoncms/test_config_flow.py index fa8ae7ce0682f2..3157ccdd574a87 100644 --- a/tests/components/emoncms/test_config_flow.py +++ b/tests/components/emoncms/test_config_flow.py @@ -2,14 +2,20 @@ from unittest.mock import AsyncMock -from homeassistant.components.emoncms.const import CONF_ONLY_INCLUDE_FEEDID, DOMAIN +from homeassistant.components.emoncms.const import ( + CONF_ONLY_INCLUDE_FEEDID, + DOMAIN, + SYNC_MODE, + SYNC_MODE_AUTO, + SYNC_MODE_MANUAL, +) from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_URL from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from . import setup_integration -from .conftest import EMONCMS_FAILURE, SENSOR_NAME +from .conftest import EMONCMS_FAILURE, FLOW_RESULT, SENSOR_NAME from tests.common import MockConfigEntry @@ -19,25 +25,41 @@ } -async def test_user_flow( - hass: HomeAssistant, - mock_setup_entry: AsyncMock, - emoncms_client: AsyncMock, +async def test_user_flow_failure( + hass: HomeAssistant, emoncms_client: AsyncMock ) -> None: - """Test we get the user form.""" + """Test emoncms failure when adding a new entry.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) + emoncms_client.async_request.return_value = EMONCMS_FAILURE assert result["type"] is FlowResultType.FORM - assert result["errors"] == {} - result = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT, ) + assert result["errors"]["base"] == "api_error" + assert result["description_placeholders"]["details"] == "failure" + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + +async def test_user_flow_manual_mode( + hass: HomeAssistant, mock_setup_entry: AsyncMock, emoncms_client: AsyncMock +) -> None: + """Test we get the user forms and the entry in manual mode.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {**USER_INPUT, SYNC_MODE: SYNC_MODE_MANUAL}, + ) + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ONLY_INCLUDE_FEEDID: ["1"]}, @@ -46,14 +68,30 @@ async def test_user_flow( assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == SENSOR_NAME assert result["data"] == {**USER_INPUT, CONF_ONLY_INCLUDE_FEEDID: ["1"]} - assert len(mock_setup_entry.mock_calls) == 1 + # assert len(mock_setup_entry.mock_calls) == 1 -CONFIG_ENTRY = { - CONF_API_KEY: "my_api_key", - CONF_ONLY_INCLUDE_FEEDID: ["1"], - CONF_URL: "http://1.1.1.1", -} +async def test_user_flow_auto_mode( + hass: HomeAssistant, mock_setup_entry: AsyncMock, emoncms_client: AsyncMock +) -> None: + """Test we get the user form and the entry in automatic mode.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {**USER_INPUT, SYNC_MODE: SYNC_MODE_AUTO}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == SENSOR_NAME + assert result["data"] == { + **USER_INPUT, + CONF_ONLY_INCLUDE_FEEDID: FLOW_RESULT[CONF_ONLY_INCLUDE_FEEDID], + } + assert len(mock_setup_entry.mock_calls) == 1 async def test_options_flow( @@ -80,13 +118,12 @@ async def test_options_flow( async def test_options_flow_failure( hass: HomeAssistant, - mock_setup_entry: AsyncMock, emoncms_client: AsyncMock, config_entry: MockConfigEntry, ) -> None: """Options flow - test failure.""" - emoncms_client.async_request.return_value = EMONCMS_FAILURE await setup_integration(hass, config_entry) + emoncms_client.async_request.return_value = EMONCMS_FAILURE result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() assert result["errors"]["base"] == "api_error" From c17ee0d1232046670d23e6b58eb39b450b923453 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 30 Jun 2025 10:06:05 +0200 Subject: [PATCH 3/7] Allow binary sensor template to return state unknown (#128861) * Allow binary sensor template to return state unknown * Add tests * Adjust TriggerBinarySensorEntity * Add restore tests for BinarySensorTemplate * Add tests for TriggerBinarySensorEntity * Tweak * Tweak * Adjust tests * Adjust --- .../components/template/binary_sensor.py | 18 ++++---- .../components/template/test_binary_sensor.py | 44 +++++++++++++------ 2 files changed, 39 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index f0ec64eae2a45d..b3bbf37712f457 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -303,11 +303,9 @@ def _update_state(self, result): self._delay_cancel() self._delay_cancel = None - state = ( - None - if isinstance(result, TemplateError) - else template.result_as_boolean(result) - ) + state: bool | None = None + if result is not None and not isinstance(result, TemplateError): + state = template.result_as_boolean(result) if state == self._attr_is_on: return @@ -347,7 +345,7 @@ def __init__( """Initialize the entity.""" super().__init__(hass, coordinator, config) - for key in (CONF_DELAY_ON, CONF_DELAY_OFF, CONF_AUTO_OFF): + for key in (CONF_STATE, CONF_DELAY_ON, CONF_DELAY_OFF, CONF_AUTO_OFF): if isinstance(config.get(key), template.Template): self._to_render_simple.append(key) self._parse_result.add(key) @@ -391,7 +389,9 @@ def _handle_coordinator_update(self) -> None: self._process_data() raw = self._rendered.get(CONF_STATE) - state = template.result_as_boolean(raw) + state: bool | None = None + if raw is not None: + state = template.result_as_boolean(raw) key = CONF_DELAY_ON if state else CONF_DELAY_OFF delay = self._rendered.get(key) or self._config.get(key) @@ -417,8 +417,8 @@ def _handle_coordinator_update(self) -> None: self.async_write_ha_state() return - # state without delay. None means rendering failed. - if self._attr_is_on == state or state is None or delay is None: + # state without delay. + if self._attr_is_on == state or delay is None: self._set_state(state) return diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index 29ef524a4ab585..a3b7edea919304 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -253,7 +253,7 @@ async def test_setup_invalid_sensors(hass: HomeAssistant, count: int) -> None: @pytest.mark.parametrize( ("state_template", "expected_result"), [ - ("{{ None }}", STATE_OFF), + ("{{ None }}", STATE_UNKNOWN), ("{{ True }}", STATE_ON), ("{{ False }}", STATE_OFF), ("{{ 1 }}", STATE_ON), @@ -263,7 +263,7 @@ async def test_setup_invalid_sensors(hass: HomeAssistant, count: int) -> None: "{% else %}" "{{ states('binary_sensor.three') == 'off' }}" "{% endif %}", - STATE_OFF, + STATE_UNKNOWN, ), ("{{ 1 / 0 == 10 }}", STATE_UNAVAILABLE), ], @@ -1090,18 +1090,18 @@ async def test_availability_icon_picture(hass: HomeAssistant, entity_id: str) -> ({"delay_on": 5}, STATE_ON, STATE_OFF, STATE_OFF), ({"delay_on": 5}, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN), ({"delay_on": 5}, STATE_ON, STATE_UNKNOWN, STATE_UNKNOWN), - ({}, None, STATE_ON, STATE_OFF), - ({}, None, STATE_OFF, STATE_OFF), - ({}, None, STATE_UNAVAILABLE, STATE_OFF), - ({}, None, STATE_UNKNOWN, STATE_OFF), - ({"delay_off": 5}, None, STATE_ON, STATE_ON), - ({"delay_off": 5}, None, STATE_OFF, STATE_OFF), + ({}, None, STATE_ON, STATE_UNKNOWN), + ({}, None, STATE_OFF, STATE_UNKNOWN), + ({}, None, STATE_UNAVAILABLE, STATE_UNKNOWN), + ({}, None, STATE_UNKNOWN, STATE_UNKNOWN), + ({"delay_off": 5}, None, STATE_ON, STATE_UNKNOWN), + ({"delay_off": 5}, None, STATE_OFF, STATE_UNKNOWN), ({"delay_off": 5}, None, STATE_UNAVAILABLE, STATE_UNKNOWN), ({"delay_off": 5}, None, STATE_UNKNOWN, STATE_UNKNOWN), - ({"delay_on": 5}, None, STATE_ON, STATE_OFF), - ({"delay_on": 5}, None, STATE_OFF, STATE_OFF), - ({"delay_on": 5}, None, STATE_UNAVAILABLE, STATE_OFF), - ({"delay_on": 5}, None, STATE_UNKNOWN, STATE_OFF), + ({"delay_on": 5}, None, STATE_ON, STATE_UNKNOWN), + ({"delay_on": 5}, None, STATE_OFF, STATE_UNKNOWN), + ({"delay_on": 5}, None, STATE_UNAVAILABLE, STATE_UNKNOWN), + ({"delay_on": 5}, None, STATE_UNKNOWN, STATE_UNKNOWN), ], ) async def test_restore_state( @@ -1209,7 +1209,7 @@ async def test_restore_state( [ (2, STATE_ON, "mdi:pirate", "/local/dogs.png", 3, 1, "si"), (1, STATE_OFF, "mdi:pirate", "/local/dogs.png", 2, 1, "si"), - (0, STATE_OFF, "mdi:pirate", "/local/dogs.png", 1, 1, "si"), + (0, STATE_UNKNOWN, "mdi:pirate", "/local/dogs.png", 1, 1, "si"), (-1, STATE_UNAVAILABLE, None, None, None, None, None), ], ) @@ -1273,6 +1273,22 @@ async def test_trigger_entity( assert state.state == final_state assert state.attributes.get("another") == another_attr_update + # Check None values + hass.bus.async_fire("test_event", {"beer": 0}) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.hello_name") + assert state.state == STATE_UNKNOWN + state = hass.states.get("binary_sensor.via_list") + assert state.state == STATE_UNKNOWN + + # Check impossible values + hass.bus.async_fire("test_event", {"beer": -1}) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.hello_name") + assert state.state == STATE_UNAVAILABLE + state = hass.states.get("binary_sensor.via_list") + assert state.state == STATE_UNAVAILABLE + @pytest.mark.parametrize(("count", "domain"), [(1, "template")]) @pytest.mark.parametrize( @@ -1298,7 +1314,7 @@ async def test_trigger_entity( [ (2, STATE_UNKNOWN, STATE_ON, STATE_OFF), (1, STATE_OFF, STATE_OFF, STATE_OFF), - (0, STATE_OFF, STATE_OFF, STATE_OFF), + (0, STATE_UNKNOWN, STATE_UNKNOWN, STATE_UNKNOWN), (-1, STATE_UNAVAILABLE, STATE_UNAVAILABLE, STATE_UNAVAILABLE), ], ) From c7603b39eca8b16075184275046dec06d0c5327d Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Mon, 30 Jun 2025 10:44:39 +0200 Subject: [PATCH 4/7] Fix inputs to correctly handle Fahrenheit in IronOS (#135421) * Fix inputs to correctly handle Fahrenheit in IronOS * some refactoring * add boost switch entity * Revert switch entity * refactor * remove commented code * some changes --- homeassistant/components/iron_os/const.py | 4 + .../components/iron_os/coordinator.py | 4 +- homeassistant/components/iron_os/number.py | 215 ++++++++++++------ .../iron_os/snapshots/test_number.ambr | 13 +- tests/components/iron_os/test_number.py | 48 +++- 5 files changed, 205 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/iron_os/const.py b/homeassistant/components/iron_os/const.py index 34889636808dcb..0ed645f8f7b58e 100644 --- a/homeassistant/components/iron_os/const.py +++ b/homeassistant/components/iron_os/const.py @@ -10,4 +10,8 @@ DISCOVERY_SVC_UUID = "9eae1000-9d0d-48c5-aa55-33e27f9bc533" MAX_TEMP: int = 450 +MAX_TEMP_F: int = 850 MIN_TEMP: int = 10 +MIN_TEMP_F: int = 50 +MIN_BOOST_TEMP: int = 250 +MIN_BOOST_TEMP_F: int = 480 diff --git a/homeassistant/components/iron_os/coordinator.py b/homeassistant/components/iron_os/coordinator.py index 99c688ea85507e..7214db0a12f4fc 100644 --- a/homeassistant/components/iron_os/coordinator.py +++ b/homeassistant/components/iron_os/coordinator.py @@ -168,7 +168,9 @@ async def _async_update_data(self) -> SettingsDataResponse: if self.device.is_connected and characteristics: try: - return await self.device.get_settings(list(characteristics)) + return await self.device.get_settings( + list(characteristics | {CharSetting.TEMP_UNIT}) + ) except CommunicationError as e: _LOGGER.debug("Failed to fetch settings", exc_info=e) diff --git a/homeassistant/components/iron_os/number.py b/homeassistant/components/iron_os/number.py index 6ad5947cb6f4f1..9fada23a9873e3 100644 --- a/homeassistant/components/iron_os/number.py +++ b/homeassistant/components/iron_os/number.py @@ -6,10 +6,9 @@ from dataclasses import dataclass from enum import StrEnum -from pynecil import CharSetting, LiveDataResponse, SettingsDataResponse +from pynecil import CharSetting, LiveDataResponse, SettingsDataResponse, TempUnit from homeassistant.components.number import ( - DEFAULT_MAX_VALUE, NumberDeviceClass, NumberEntity, NumberEntityDescription, @@ -24,9 +23,17 @@ ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.util.unit_conversion import TemperatureConverter from . import IronOSConfigEntry -from .const import MAX_TEMP, MIN_TEMP +from .const import ( + MAX_TEMP, + MAX_TEMP_F, + MIN_BOOST_TEMP, + MIN_BOOST_TEMP_F, + MIN_TEMP, + MIN_TEMP_F, +) from .coordinator import IronOSCoordinators from .entity import IronOSBaseEntity @@ -38,9 +45,10 @@ class IronOSNumberEntityDescription(NumberEntityDescription): """Describes IronOS number entity.""" value_fn: Callable[[LiveDataResponse, SettingsDataResponse], float | int | None] - max_value_fn: Callable[[LiveDataResponse], float | int] | None = None characteristic: CharSetting raw_value_fn: Callable[[float], float | int] | None = None + native_max_value_f: float | None = None + native_min_value_f: float | None = None class PinecilNumber(StrEnum): @@ -74,44 +82,6 @@ def multiply(value: float | None, multiplier: float) -> float | None: PINECIL_NUMBER_DESCRIPTIONS: tuple[IronOSNumberEntityDescription, ...] = ( - IronOSNumberEntityDescription( - key=PinecilNumber.SETPOINT_TEMP, - translation_key=PinecilNumber.SETPOINT_TEMP, - native_unit_of_measurement=UnitOfTemperature.CELSIUS, - device_class=NumberDeviceClass.TEMPERATURE, - value_fn=lambda data, _: data.setpoint_temp, - characteristic=CharSetting.SETPOINT_TEMP, - mode=NumberMode.BOX, - native_min_value=MIN_TEMP, - native_step=5, - max_value_fn=lambda data: min(data.max_tip_temp_ability or MAX_TEMP, MAX_TEMP), - ), - IronOSNumberEntityDescription( - key=PinecilNumber.SLEEP_TEMP, - translation_key=PinecilNumber.SLEEP_TEMP, - native_unit_of_measurement=UnitOfTemperature.CELSIUS, - device_class=NumberDeviceClass.TEMPERATURE, - value_fn=lambda _, settings: settings.get("sleep_temp"), - characteristic=CharSetting.SLEEP_TEMP, - mode=NumberMode.BOX, - native_min_value=MIN_TEMP, - native_max_value=MAX_TEMP, - native_step=10, - entity_category=EntityCategory.CONFIG, - ), - IronOSNumberEntityDescription( - key=PinecilNumber.BOOST_TEMP, - translation_key=PinecilNumber.BOOST_TEMP, - native_unit_of_measurement=UnitOfTemperature.CELSIUS, - device_class=NumberDeviceClass.TEMPERATURE, - value_fn=lambda _, settings: settings.get("boost_temp"), - characteristic=CharSetting.BOOST_TEMP, - mode=NumberMode.BOX, - native_min_value=0, - native_max_value=MAX_TEMP, - native_step=10, - entity_category=EntityCategory.CONFIG, - ), IronOSNumberEntityDescription( key=PinecilNumber.QC_MAX_VOLTAGE, translation_key=PinecilNumber.QC_MAX_VOLTAGE, @@ -296,6 +266,61 @@ def multiply(value: float | None, multiplier: float) -> float | None: entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, ), +) + +PINECIL_NUMBER_DESCRIPTIONS_V223: tuple[IronOSNumberEntityDescription, ...] = ( + IronOSNumberEntityDescription( + key=PinecilNumber.HALL_EFFECT_SLEEP_TIME, + translation_key=PinecilNumber.HALL_EFFECT_SLEEP_TIME, + value_fn=(lambda _, settings: settings.get("hall_sleep_time")), + characteristic=CharSetting.HALL_SLEEP_TIME, + raw_value_fn=lambda value: value, + mode=NumberMode.BOX, + native_min_value=0, + native_max_value=60, + native_step=5, + entity_category=EntityCategory.CONFIG, + native_unit_of_measurement=UnitOfTime.SECONDS, + entity_registry_enabled_default=False, + ), +) + +""" +The `device_class` attribute was removed from the `setpoint_temperature`, `sleep_temperature`, and `boost_temp` entities. +These entities represent user-defined input values, not measured temperatures, and their +interpretation depends on the device's current unit configuration. Applying a device_class +results in automatic unit conversions, which introduce rounding errors due to the use of integers. +This can prevent the correct value from being set, as the input is modified during synchronization with the device. +""" +PINECIL_TEMP_NUMBER_DESCRIPTIONS: tuple[IronOSNumberEntityDescription, ...] = ( + IronOSNumberEntityDescription( + key=PinecilNumber.SLEEP_TEMP, + translation_key=PinecilNumber.SLEEP_TEMP, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_fn=lambda _, settings: settings.get("sleep_temp"), + characteristic=CharSetting.SLEEP_TEMP, + mode=NumberMode.BOX, + native_min_value=MIN_TEMP, + native_max_value=MAX_TEMP, + native_min_value_f=MIN_TEMP_F, + native_max_value_f=MAX_TEMP_F, + native_step=10, + entity_category=EntityCategory.CONFIG, + ), + IronOSNumberEntityDescription( + key=PinecilNumber.BOOST_TEMP, + translation_key=PinecilNumber.BOOST_TEMP, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_fn=lambda _, settings: settings.get("boost_temp"), + characteristic=CharSetting.BOOST_TEMP, + mode=NumberMode.BOX, + native_min_value=MIN_BOOST_TEMP, + native_min_value_f=MIN_BOOST_TEMP_F, + native_max_value=MAX_TEMP, + native_max_value_f=MAX_TEMP_F, + native_step=10, + entity_category=EntityCategory.CONFIG, + ), IronOSNumberEntityDescription( key=PinecilNumber.TEMP_INCREMENT_SHORT, translation_key=PinecilNumber.TEMP_INCREMENT_SHORT, @@ -307,7 +332,6 @@ def multiply(value: float | None, multiplier: float) -> float | None: native_max_value=50, native_step=1, entity_category=EntityCategory.CONFIG, - native_unit_of_measurement=UnitOfTemperature.CELSIUS, ), IronOSNumberEntityDescription( key=PinecilNumber.TEMP_INCREMENT_LONG, @@ -320,25 +344,21 @@ def multiply(value: float | None, multiplier: float) -> float | None: native_max_value=90, native_step=5, entity_category=EntityCategory.CONFIG, - native_unit_of_measurement=UnitOfTemperature.CELSIUS, ), ) -PINECIL_NUMBER_DESCRIPTIONS_V223: tuple[IronOSNumberEntityDescription, ...] = ( - IronOSNumberEntityDescription( - key=PinecilNumber.HALL_EFFECT_SLEEP_TIME, - translation_key=PinecilNumber.HALL_EFFECT_SLEEP_TIME, - value_fn=(lambda _, settings: settings.get("hall_sleep_time")), - characteristic=CharSetting.HALL_SLEEP_TIME, - raw_value_fn=lambda value: value, - mode=NumberMode.BOX, - native_min_value=0, - native_max_value=60, - native_step=5, - entity_category=EntityCategory.CONFIG, - native_unit_of_measurement=UnitOfTime.SECONDS, - entity_registry_enabled_default=False, - ), +PINECIL_SETPOINT_NUMBER_DESCRIPTION = IronOSNumberEntityDescription( + key=PinecilNumber.SETPOINT_TEMP, + translation_key=PinecilNumber.SETPOINT_TEMP, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_fn=lambda data, _: data.setpoint_temp, + characteristic=CharSetting.SETPOINT_TEMP, + mode=NumberMode.BOX, + native_min_value=MIN_TEMP, + native_max_value=MAX_TEMP, + native_min_value_f=MIN_TEMP_F, + native_max_value_f=MAX_TEMP_F, + native_step=5, ) @@ -354,9 +374,18 @@ async def async_setup_entry( if coordinators.live_data.v223_features: descriptions += PINECIL_NUMBER_DESCRIPTIONS_V223 - async_add_entities( + entities = [ IronOSNumberEntity(coordinators, description) for description in descriptions + ] + + entities.extend( + IronOSTemperatureNumberEntity(coordinators, description) + for description in PINECIL_TEMP_NUMBER_DESCRIPTIONS + ) + entities.append( + IronOSSetpointNumberEntity(coordinators, PINECIL_SETPOINT_NUMBER_DESCRIPTION) ) + async_add_entities(entities) class IronOSNumberEntity(IronOSBaseEntity, NumberEntity): @@ -388,15 +417,6 @@ def native_value(self) -> float | int | None: self.coordinator.data, self.settings.data ) - @property - def native_max_value(self) -> float: - """Return sensor state.""" - - if self.entity_description.max_value_fn is not None: - return self.entity_description.max_value_fn(self.coordinator.data) - - return self.entity_description.native_max_value or DEFAULT_MAX_VALUE - async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" @@ -407,3 +427,60 @@ async def async_added_to_hass(self) -> None: ) ) await self.settings.async_request_refresh() + + +class IronOSTemperatureNumberEntity(IronOSNumberEntity): + """Implementation of a IronOS temperature number entity.""" + + @property + def native_unit_of_measurement(self) -> str | None: + """Return the unit of measurement of the sensor, if any.""" + + return ( + UnitOfTemperature.FAHRENHEIT + if self.settings.data.get("temp_unit") is TempUnit.FAHRENHEIT + else UnitOfTemperature.CELSIUS + ) + + @property + def native_min_value(self) -> float: + """Return the minimum value.""" + + return ( + self.entity_description.native_min_value_f + if self.entity_description.native_min_value_f + and self.native_unit_of_measurement is UnitOfTemperature.FAHRENHEIT + else super().native_min_value + ) + + @property + def native_max_value(self) -> float: + """Return the maximum value.""" + + return ( + self.entity_description.native_max_value_f + if self.entity_description.native_max_value_f + and self.native_unit_of_measurement is UnitOfTemperature.FAHRENHEIT + else super().native_max_value + ) + + +class IronOSSetpointNumberEntity(IronOSTemperatureNumberEntity): + """IronOS setpoint temperature entity.""" + + @property + def native_max_value(self) -> float: + """Return the maximum value.""" + + return ( + min( + TemperatureConverter.convert( + float(max_tip_c), + UnitOfTemperature.CELSIUS, + self.native_unit_of_measurement, + ), + super().native_max_value, + ) + if (max_tip_c := self.coordinator.data.max_tip_temp_ability) is not None + else super().native_max_value + ) diff --git a/tests/components/iron_os/snapshots/test_number.ambr b/tests/components/iron_os/snapshots/test_number.ambr index 37d8b1f481929c..52fd6bb2ce4068 100644 --- a/tests/components/iron_os/snapshots/test_number.ambr +++ b/tests/components/iron_os/snapshots/test_number.ambr @@ -6,7 +6,7 @@ 'area_id': None, 'capabilities': dict({ 'max': 450, - 'min': 0, + 'min': 250, 'mode': , 'step': 10, }), @@ -27,7 +27,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, 'original_name': 'Boost temperature', 'platform': 'iron_os', @@ -42,10 +42,9 @@ # name: test_state[number.pinecil_boost_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', 'friendly_name': 'Pinecil Boost temperature', 'max': 450, - 'min': 0, + 'min': 250, 'mode': , 'step': 10, 'unit_of_measurement': , @@ -839,7 +838,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, 'original_name': 'Setpoint temperature', 'platform': 'iron_os', @@ -854,7 +853,6 @@ # name: test_state[number.pinecil_setpoint_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', 'friendly_name': 'Pinecil Setpoint temperature', 'max': 450, 'min': 10, @@ -1015,7 +1013,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, 'original_name': 'Sleep temperature', 'platform': 'iron_os', @@ -1030,7 +1028,6 @@ # name: test_state[number.pinecil_sleep_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', 'friendly_name': 'Pinecil Sleep temperature', 'max': 450, 'min': 10, diff --git a/tests/components/iron_os/test_number.py b/tests/components/iron_os/test_number.py index 9a4ba53f3381af..3c7be52c5778d2 100644 --- a/tests/components/iron_os/test_number.py +++ b/tests/components/iron_os/test_number.py @@ -5,10 +5,15 @@ from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory -from pynecil import CharSetting, CommunicationError +from pynecil import CharSetting, CommunicationError, TempUnit import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.components.iron_os.const import ( + MAX_TEMP_F, + MIN_BOOST_TEMP_F, + MIN_TEMP_F, +) from homeassistant.components.number import ( ATTR_VALUE, DOMAIN as NUMBER_DOMAIN, @@ -56,6 +61,47 @@ async def test_state( await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) +@pytest.mark.parametrize( + ("entity_id", "min_value", "max_value"), + [ + ("number.pinecil_setpoint_temperature", MIN_TEMP_F, MAX_TEMP_F), + ("number.pinecil_boost_temperature", MIN_BOOST_TEMP_F, MAX_TEMP_F), + ("number.pinecil_long_press_temperature_step", 5, 90), + ("number.pinecil_short_press_temperature_step", 1, 50), + ("number.pinecil_sleep_temperature", MIN_TEMP_F, MAX_TEMP_F), + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default", "ble_device") +async def test_state_fahrenheit( + hass: HomeAssistant, + config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, + mock_pynecil: AsyncMock, + entity_id: str, + min_value: int, + max_value: int, +) -> None: + """Test with temp unit set to fahrenheit.""" + + mock_pynecil.get_settings.return_value["temp_unit"] = TempUnit.FAHRENHEIT + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + freezer.tick(timedelta(seconds=3)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state is not None + + assert state.attributes["min"] == min_value + assert state.attributes["max"] == max_value + + @pytest.mark.parametrize( ("entity_id", "characteristic", "value", "expected_value"), [ From 4d58024d5d13a334a4142a37a608cf72bb7b1326 Mon Sep 17 00:00:00 2001 From: Steffen Rusitschka Date: Mon, 30 Jun 2025 10:52:33 +0200 Subject: [PATCH 5/7] Add publish_string_states config to zabbix (#134773) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add include_strings config to zabbix * Remove commented code * Fix ruff formatting * Update homeassistant/components/zabbix/__init__.py Co-authored-by: Abílio Costa * Update homeassistant/components/zabbix/__init__.py Co-authored-by: Abílio Costa * Don't use dict.get, CONF_INCLUDE_STRINGS has a default value and will always be set. Co-authored-by: Erik Montnemery * Convert to string only when include_strings is true Co-authored-by: Erik Montnemery * change to guard * Fix review comments * ruff, mypy, pylint fixes * more ruff, mypy fixes * and another ruff format fix --------- Co-authored-by: Abílio Costa Co-authored-by: Erik Montnemery --- homeassistant/components/zabbix/__init__.py | 58 +++++++++++++-------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/zabbix/__init__.py b/homeassistant/components/zabbix/__init__.py index 524bac271de0af..31a09242a71429 100644 --- a/homeassistant/components/zabbix/__init__.py +++ b/homeassistant/components/zabbix/__init__.py @@ -43,6 +43,7 @@ _LOGGER = logging.getLogger(__name__) CONF_PUBLISH_STATES_HOST = "publish_states_host" +CONF_PUBLISH_STRING_STATES = "publish_string_states" DEFAULT_SSL = False DEFAULT_PATH = "zabbix" @@ -67,6 +68,7 @@ vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_PUBLISH_STATES_HOST): cv.string, + vol.Optional(CONF_PUBLISH_STRING_STATES, default=False): cv.boolean, } ) }, @@ -85,6 +87,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: password = conf.get(CONF_PASSWORD) publish_states_host = conf.get(CONF_PUBLISH_STATES_HOST) + publish_string_states = conf[CONF_PUBLISH_STRING_STATES] entities_filter = convert_include_exclude_filter(conf) @@ -107,6 +110,28 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.data[DOMAIN] = zapi + def update_metrics( + metrics: list[ItemValue], + item_type: str, + keys: set[str], + key_values: dict[str, float | str], + ): + keys_count = len(keys) + keys.update(key_values) + if len(keys) > keys_count: + discovery = [{"{#KEY}": key} for key in keys] + metric = ItemValue( + publish_states_host, + f"homeassistant.{item_type}s_discovery", + json.dumps(discovery), + ) + metrics.append(metric) + for key, value in key_values.items(): + metric = ItemValue( + publish_states_host, f"homeassistant.{item_type}[{key}]", value + ) + metrics.append(metric) + def event_to_metrics( event: Event, float_keys: set[str], string_keys: set[str] ) -> list[ItemValue] | None: @@ -119,8 +144,8 @@ def event_to_metrics( if not entities_filter(entity_id): return None - floats = {} - strings = {} + floats: dict[str, float | str] = {} + strings: dict[str, float | str] = {} try: _state_as_value = float(state.state) floats[entity_id] = _state_as_value @@ -129,7 +154,8 @@ def event_to_metrics( _state_as_value = float(state_helper.state_as_number(state)) floats[entity_id] = _state_as_value except ValueError: - strings[entity_id] = state.state + if publish_string_states: + strings[entity_id] = str(state.state) for key, value in state.attributes.items(): # For each value we try to cast it as float @@ -141,28 +167,18 @@ def event_to_metrics( except (ValueError, TypeError): float_value = None if float_value is None or not math.isfinite(float_value): - strings[attribute_id] = str(value) + # Don't store string attributes for now + pass else: floats[attribute_id] = float_value - metrics = [] - float_keys_count = len(float_keys) - float_keys.update(floats) - if len(float_keys) != float_keys_count: - floats_discovery = [{"{#KEY}": float_key} for float_key in float_keys] - metric = ItemValue( - publish_states_host, - "homeassistant.floats_discovery", - json.dumps(floats_discovery), - ) - metrics.append(metric) - for key, value in floats.items(): - metric = ItemValue( - publish_states_host, f"homeassistant.float[{key}]", value - ) - metrics.append(metric) + metrics: list[ItemValue] = [] + update_metrics(metrics, "float", float_keys, floats) + + if not publish_string_states: + return metrics - string_keys.update(strings) + update_metrics(metrics, "string", string_keys, strings) return metrics if publish_states_host: From a6e3da43cabfc43ee3e269e7c48dbce4e458d81c Mon Sep 17 00:00:00 2001 From: Evan Severson <208220+eseverson@users.noreply.github.com> Date: Mon, 30 Jun 2025 02:08:50 -0700 Subject: [PATCH 6/7] Fixed pushbullet handling of fields longer than 255 characters (#146993) --- homeassistant/components/pushbullet/sensor.py | 9 +- tests/components/pushbullet/test_sensor.py | 168 ++++++++++++++++++ 2 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 tests/components/pushbullet/test_sensor.py diff --git a/homeassistant/components/pushbullet/sensor.py b/homeassistant/components/pushbullet/sensor.py index 2dbaa8fc713b74..ea9a8f198ef47a 100644 --- a/homeassistant/components/pushbullet/sensor.py +++ b/homeassistant/components/pushbullet/sensor.py @@ -4,7 +4,7 @@ from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, MAX_LENGTH_STATE_STATE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -116,7 +116,12 @@ def async_update_callback(self) -> None: attributes into self._state_attributes. """ try: - self._attr_native_value = self.pb_provider.data[self.entity_description.key] + value = self.pb_provider.data[self.entity_description.key] + # Truncate state value to MAX_LENGTH_STATE_STATE while preserving full content in attributes + if isinstance(value, str) and len(value) > MAX_LENGTH_STATE_STATE: + self._attr_native_value = value[: MAX_LENGTH_STATE_STATE - 3] + "..." + else: + self._attr_native_value = value self._attr_extra_state_attributes = self.pb_provider.data except (KeyError, TypeError): pass diff --git a/tests/components/pushbullet/test_sensor.py b/tests/components/pushbullet/test_sensor.py new file mode 100644 index 00000000000000..b6ae8c3a211d57 --- /dev/null +++ b/tests/components/pushbullet/test_sensor.py @@ -0,0 +1,168 @@ +"""Test pushbullet sensor platform.""" + +from unittest.mock import Mock + +import pytest + +from homeassistant.components.pushbullet.const import DOMAIN +from homeassistant.components.pushbullet.sensor import ( + SENSOR_TYPES, + PushBulletNotificationSensor, +) +from homeassistant.const import MAX_LENGTH_STATE_STATE +from homeassistant.core import HomeAssistant + +from . import MOCK_CONFIG + +from tests.common import MockConfigEntry + + +def _create_mock_provider() -> Mock: + """Create a mock pushbullet provider for testing.""" + mock_provider = Mock() + mock_provider.pushbullet.user_info = {"iden": "test_user_123"} + return mock_provider + + +def _get_sensor_description(key: str): + """Get sensor description by key.""" + for desc in SENSOR_TYPES: + if desc.key == key: + return desc + raise ValueError(f"Sensor description not found for key: {key}") + + +def _create_test_sensor( + provider: Mock, sensor_key: str +) -> PushBulletNotificationSensor: + """Create a test sensor instance with mocked dependencies.""" + description = _get_sensor_description(sensor_key) + sensor = PushBulletNotificationSensor( + name="Test Pushbullet", pb_provider=provider, description=description + ) + # Mock async_write_ha_state to avoid requiring full HA setup + sensor.async_write_ha_state = Mock() + return sensor + + +@pytest.fixture +async def mock_pushbullet_entry(hass: HomeAssistant, requests_mock_fixture): + """Set up pushbullet integration.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + return entry + + +def test_sensor_truncation_logic() -> None: + """Test sensor truncation logic for body sensor.""" + provider = _create_mock_provider() + sensor = _create_test_sensor(provider, "body") + + # Test long body truncation + long_body = "a" * (MAX_LENGTH_STATE_STATE + 50) + provider.data = { + "body": long_body, + "title": "Test Title", + "type": "note", + } + + sensor.async_update_callback() + + # Verify truncation + assert len(sensor._attr_native_value) == MAX_LENGTH_STATE_STATE + assert sensor._attr_native_value.endswith("...") + assert sensor._attr_native_value.startswith("a") + assert sensor._attr_extra_state_attributes["body"] == long_body + + # Test normal length body + normal_body = "This is a normal body" + provider.data = { + "body": normal_body, + "title": "Test Title", + "type": "note", + } + + sensor.async_update_callback() + + # Verify no truncation + assert sensor._attr_native_value == normal_body + assert len(sensor._attr_native_value) < MAX_LENGTH_STATE_STATE + assert sensor._attr_extra_state_attributes["body"] == normal_body + + # Test exactly max length + exact_body = "a" * MAX_LENGTH_STATE_STATE + provider.data = { + "body": exact_body, + "title": "Test Title", + "type": "note", + } + + sensor.async_update_callback() + + # Verify no truncation at the limit + assert sensor._attr_native_value == exact_body + assert len(sensor._attr_native_value) == MAX_LENGTH_STATE_STATE + assert sensor._attr_extra_state_attributes["body"] == exact_body + + +def test_sensor_truncation_title_sensor() -> None: + """Test sensor truncation logic on title sensor.""" + provider = _create_mock_provider() + sensor = _create_test_sensor(provider, "title") + + # Test long title truncation + long_title = "Title " + "x" * (MAX_LENGTH_STATE_STATE) + provider.data = { + "body": "Test body", + "title": long_title, + "type": "note", + } + + sensor.async_update_callback() + + # Verify truncation + assert len(sensor._attr_native_value) == MAX_LENGTH_STATE_STATE + assert sensor._attr_native_value.endswith("...") + assert sensor._attr_native_value.startswith("Title") + assert sensor._attr_extra_state_attributes["title"] == long_title + + +def test_sensor_truncation_non_string_handling() -> None: + """Test that non-string values are handled correctly.""" + provider = _create_mock_provider() + sensor = _create_test_sensor(provider, "body") + + # Test with None value + provider.data = { + "body": None, + "title": "Test Title", + "type": "note", + } + + sensor.async_update_callback() + assert sensor._attr_native_value is None + + # Test with integer value (would be converted to string by Home Assistant) + provider.data = { + "body": 12345, + "title": "Test Title", + "type": "note", + } + + sensor.async_update_callback() + assert sensor._attr_native_value == 12345 # Not truncated since it's not a string + + # Test with missing key + provider.data = { + "title": "Test Title", + "type": "note", + } + + # This should not raise an exception + sensor.async_update_callback() From c7b2f236be23d15d081f4378a21f708656cc7e62 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Jun 2025 11:15:12 +0200 Subject: [PATCH 7/7] Type Z-Wave JS config entry (#147456) * Type Z-Wave JS config entry * Migrate to data class --- homeassistant/components/zwave_js/__init__.py | 31 +++---- homeassistant/components/zwave_js/api.py | 82 +++++++++++-------- .../components/zwave_js/binary_sensor.py | 17 ++-- homeassistant/components/zwave_js/button.py | 13 ++- homeassistant/components/zwave_js/climate.py | 13 ++- .../components/zwave_js/config_flow.py | 7 +- homeassistant/components/zwave_js/const.py | 2 - homeassistant/components/zwave_js/cover.py | 22 ++--- .../zwave_js/device_automation_helpers.py | 5 +- .../components/zwave_js/diagnostics.py | 15 ++-- homeassistant/components/zwave_js/event.py | 11 ++- homeassistant/components/zwave_js/fan.py | 15 ++-- homeassistant/components/zwave_js/helpers.py | 34 ++++---- .../components/zwave_js/humidifier.py | 11 ++- homeassistant/components/zwave_js/light.py | 13 ++- homeassistant/components/zwave_js/lock.py | 8 +- homeassistant/components/zwave_js/models.py | 27 ++++++ homeassistant/components/zwave_js/number.py | 15 ++-- homeassistant/components/zwave_js/select.py | 19 ++--- homeassistant/components/zwave_js/sensor.py | 22 +++-- homeassistant/components/zwave_js/services.py | 2 +- homeassistant/components/zwave_js/siren.py | 11 ++- homeassistant/components/zwave_js/switch.py | 17 ++-- .../components/zwave_js/triggers/event.py | 4 +- .../zwave_js/triggers/trigger_helpers.py | 6 +- homeassistant/components/zwave_js/update.py | 9 +- 26 files changed, 220 insertions(+), 211 deletions(-) create mode 100644 homeassistant/components/zwave_js/models.py diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 0b172c207155ee..982525be778f0a 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -29,7 +29,7 @@ from homeassistant.components.hassio import AddonError, AddonManager, AddonState from homeassistant.components.persistent_notification import async_create -from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_DEVICE_ID, ATTR_DOMAIN, @@ -104,7 +104,6 @@ CONF_S2_UNAUTHENTICATED_KEY, CONF_USB_PATH, CONF_USE_ADDON, - DATA_CLIENT, DOMAIN, DRIVER_READY_TIMEOUT, EVENT_DEVICE_ADDED_TO_REGISTRY, @@ -133,10 +132,10 @@ get_valueless_base_unique_id, ) from .migrate import async_migrate_discovered_value +from .models import ZwaveJSConfigEntry, ZwaveJSData from .services import async_setup_services CONNECT_TIMEOUT = 10 -DATA_DRIVER_EVENTS = "driver_events" CONFIG_SCHEMA = vol.Schema( { @@ -182,7 +181,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ZwaveJSConfigEntry) -> bool: """Set up Z-Wave JS from a config entry.""" if use_addon := entry.data.get(CONF_USE_ADDON): await async_ensure_addon_running(hass, entry) @@ -260,10 +259,12 @@ async def handle_ha_shutdown(event: Event) -> None: LOGGER.debug("Connection to Zwave JS Server initialized") - entry_runtime_data = entry.runtime_data = { - DATA_CLIENT: client, - } - entry_runtime_data[DATA_DRIVER_EVENTS] = driver_events = DriverEvents(hass, entry) + driver_events = DriverEvents(hass, entry) + entry_runtime_data = ZwaveJSData( + client=client, + driver_events=driver_events, + ) + entry.runtime_data = entry_runtime_data driver = client.driver # When the driver is ready we know it's set on the client. @@ -348,7 +349,7 @@ class DriverEvents: driver: Driver - def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + def __init__(self, hass: HomeAssistant, entry: ZwaveJSConfigEntry) -> None: """Set up the driver events instance.""" self.config_entry = entry self.dev_reg = dr.async_get(hass) @@ -1045,7 +1046,7 @@ def async_on_value_updated_fire_event( async def client_listen( hass: HomeAssistant, - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: ZwaveClient, driver_ready: asyncio.Event, ) -> None: @@ -1072,12 +1073,12 @@ async def client_listen( hass.config_entries.async_schedule_reload(entry.entry_id) -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ZwaveJSConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) entry_runtime_data = entry.runtime_data - client: ZwaveClient = entry_runtime_data[DATA_CLIENT] + client = entry_runtime_data.client if client.connected and (driver := client.driver): await async_disable_server_logging_if_needed(hass, entry, driver) @@ -1094,7 +1095,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: +async def async_remove_entry(hass: HomeAssistant, entry: ZwaveJSConfigEntry) -> None: """Remove a config entry.""" if not entry.data.get(CONF_INTEGRATION_CREATED_ADDON): return @@ -1116,7 +1117,9 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: LOGGER.error(err) -async def async_ensure_addon_running(hass: HomeAssistant, entry: ConfigEntry) -> None: +async def async_ensure_addon_running( + hass: HomeAssistant, entry: ZwaveJSConfigEntry +) -> None: """Ensure that Z-Wave JS add-on is installed and running.""" addon_manager = _get_addon_manager(hass) try: diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index a17f13e0d07e54..0f75d8b46739cc 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -7,7 +7,7 @@ from contextlib import suppress import dataclasses from functools import partial, wraps -from typing import Any, Concatenate, Literal, cast +from typing import TYPE_CHECKING, Any, Concatenate, Literal, cast from aiohttp import web, web_exceptions, web_request import voluptuous as vol @@ -70,7 +70,7 @@ ERR_UNKNOWN_ERROR, ActiveConnection, ) -from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, device_registry as dr @@ -86,7 +86,6 @@ ATTR_WAIT_FOR_RESULT, CONF_DATA_COLLECTION_OPTED_IN, CONF_INSTALLER_MODE, - DATA_CLIENT, DOMAIN, DRIVER_READY_TIMEOUT, EVENT_DEVICE_ADDED_TO_REGISTRY, @@ -102,6 +101,10 @@ get_device_id, ) +if TYPE_CHECKING: + from .models import ZwaveJSConfigEntry + + DATA_UNSUBSCRIBE = "unsubs" # general API constants @@ -254,7 +257,7 @@ async def _async_get_entry( connection: ActiveConnection, msg: dict[str, Any], entry_id: str, -) -> tuple[ConfigEntry, Client, Driver] | tuple[None, None, None]: +) -> tuple[ZwaveJSConfigEntry, Client, Driver] | tuple[None, None, None]: """Get config entry and client from message data.""" entry = hass.config_entries.async_get_entry(entry_id) if entry is None: @@ -269,7 +272,7 @@ async def _async_get_entry( ) return None, None, None - client: Client = entry.runtime_data[DATA_CLIENT] + client = entry.runtime_data.client if client.driver is None: connection.send_error( @@ -284,7 +287,14 @@ async def _async_get_entry( def async_get_entry( orig_func: Callable[ - [HomeAssistant, ActiveConnection, dict[str, Any], ConfigEntry, Client, Driver], + [ + HomeAssistant, + ActiveConnection, + dict[str, Any], + ZwaveJSConfigEntry, + Client, + Driver, + ], Coroutine[Any, Any, None], ], ) -> Callable[ @@ -726,7 +736,7 @@ async def websocket_add_node( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -903,7 +913,7 @@ async def websocket_cancel_secure_bootstrap_s2( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -926,7 +936,7 @@ async def websocket_subscribe_s2_inclusion( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -979,7 +989,7 @@ async def websocket_grant_security_classes( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -1007,7 +1017,7 @@ async def websocket_validate_dsk_and_enter_pin( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -1077,7 +1087,7 @@ async def websocket_provision_smart_start_node( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -1162,7 +1172,7 @@ async def websocket_unprovision_smart_start_node( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -1212,7 +1222,7 @@ async def websocket_get_provisioning_entries( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -1236,7 +1246,7 @@ async def websocket_parse_qr_code_string( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -1262,7 +1272,7 @@ async def websocket_try_parse_dsk_from_qr_code_string( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -1291,7 +1301,7 @@ async def websocket_lookup_device( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -1323,7 +1333,7 @@ async def websocket_supports_feature( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -1349,7 +1359,7 @@ async def websocket_stop_inclusion( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -1376,7 +1386,7 @@ async def websocket_stop_exclusion( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -1404,7 +1414,7 @@ async def websocket_remove_node( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -1692,7 +1702,7 @@ async def websocket_begin_rebuilding_routes( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -1719,7 +1729,7 @@ async def websocket_subscribe_rebuild_routes_progress( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -1772,7 +1782,7 @@ async def websocket_stop_rebuilding_routes( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -2100,7 +2110,7 @@ async def websocket_subscribe_log_updates( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -2187,7 +2197,7 @@ async def websocket_update_log_config( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -2211,7 +2221,7 @@ async def websocket_get_log_config( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -2238,7 +2248,7 @@ async def websocket_update_data_collection_preference( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -2273,7 +2283,7 @@ async def websocket_data_collection_status( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -2507,7 +2517,7 @@ async def websocket_is_any_ota_firmware_update_in_progress( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -2602,7 +2612,7 @@ async def websocket_check_for_config_updates( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -2631,7 +2641,7 @@ async def websocket_install_config_update( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -2670,7 +2680,7 @@ async def websocket_subscribe_controller_statistics( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -2823,7 +2833,7 @@ async def websocket_hard_reset_controller( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -3000,7 +3010,7 @@ async def websocket_backup_nvm( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: @@ -3062,7 +3072,7 @@ async def websocket_restore_nvm( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - entry: ConfigEntry, + entry: ZwaveJSConfigEntry, client: Client, driver: Driver, ) -> None: diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index d70690ace3101b..5b7fe4f4d7c242 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -4,7 +4,6 @@ from dataclasses import dataclass -from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import CommandClass from zwave_js_server.const.command_class.lock import DOOR_STATUS_PROPERTY from zwave_js_server.const.command_class.notification import ( @@ -18,15 +17,15 @@ BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DATA_CLIENT, DOMAIN +from .const import DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity +from .models import ZwaveJSConfigEntry PARALLEL_UPDATES = 0 @@ -364,11 +363,11 @@ def is_valid_notification_binary_sensor( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ZwaveJSConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Z-Wave binary sensor from config entry.""" - client: ZwaveClient = config_entry.runtime_data[DATA_CLIENT] + client = config_entry.runtime_data.client @callback def async_add_binary_sensor(info: ZwaveDiscoveryInfo) -> None: @@ -448,7 +447,7 @@ class ZWaveBooleanBinarySensor(ZWaveBaseEntity, BinarySensorEntity): def __init__( self, - config_entry: ConfigEntry, + config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo, ) -> None: @@ -476,7 +475,7 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity): def __init__( self, - config_entry: ConfigEntry, + config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo, state_key: str, @@ -509,7 +508,7 @@ class ZWavePropertyBinarySensor(ZWaveBaseEntity, BinarySensorEntity): def __init__( self, - config_entry: ConfigEntry, + config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo, description: PropertyZWaveJSEntityDescription, @@ -533,7 +532,7 @@ class ZWaveConfigParameterBinarySensor(ZWaveBooleanBinarySensor): _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__( - self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + self, config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize a ZWaveConfigParameterBinarySensor entity.""" super().__init__(config_entry, driver, info) diff --git a/homeassistant/components/zwave_js/button.py b/homeassistant/components/zwave_js/button.py index f3a1d5af04d65a..36bca858b50a99 100644 --- a/homeassistant/components/zwave_js/button.py +++ b/homeassistant/components/zwave_js/button.py @@ -2,32 +2,31 @@ from __future__ import annotations -from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.model.driver import Driver from zwave_js_server.model.node import Node as ZwaveNode from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, ButtonEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DATA_CLIENT, DOMAIN, LOGGER +from .const import DOMAIN, LOGGER from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity from .helpers import get_device_info, get_valueless_base_unique_id +from .models import ZwaveJSConfigEntry PARALLEL_UPDATES = 0 async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ZwaveJSConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Z-Wave button from config entry.""" - client: ZwaveClient = config_entry.runtime_data[DATA_CLIENT] + client = config_entry.runtime_data.client @callback def async_add_button(info: ZwaveDiscoveryInfo) -> None: @@ -70,7 +69,7 @@ class ZwaveBooleanNodeButton(ZWaveBaseEntity, ButtonEntity): """Representation of a ZWave button entity for a boolean value.""" def __init__( - self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + self, config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize entity.""" super().__init__(config_entry, driver, info) @@ -141,7 +140,7 @@ class ZWaveNotificationIdleButton(ZWaveBaseEntity, ButtonEntity): _attr_entity_category = EntityCategory.CONFIG def __init__( - self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + self, config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize a ZWaveNotificationIdleButton entity.""" super().__init__(config_entry, driver, info) diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 809d3543fe448b..5d3b1f8ef078b1 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -4,7 +4,6 @@ from typing import Any, cast -from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import CommandClass from zwave_js_server.const.command_class.thermostat import ( THERMOSTAT_CURRENT_TEMP_PROPERTY, @@ -31,18 +30,18 @@ HVACAction, HVACMode, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util.unit_conversion import TemperatureConverter -from .const import DATA_CLIENT, DOMAIN +from .const import DOMAIN from .discovery import ZwaveDiscoveryInfo from .discovery_data_template import DynamicCurrentTempClimateDataTemplate from .entity import ZWaveBaseEntity from .helpers import get_value_of_zwave_value +from .models import ZwaveJSConfigEntry PARALLEL_UPDATES = 0 @@ -96,11 +95,11 @@ async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ZwaveJSConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Z-Wave climate from config entry.""" - client: ZwaveClient = config_entry.runtime_data[DATA_CLIENT] + client = config_entry.runtime_data.client @callback def async_add_climate(info: ZwaveDiscoveryInfo) -> None: @@ -130,7 +129,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): _attr_precision = PRECISION_TENTHS def __init__( - self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + self, config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize thermostat.""" super().__init__(config_entry, driver, info) @@ -563,7 +562,7 @@ class DynamicCurrentTempClimate(ZWaveClimate): """Representation of a thermostat that can dynamically use a different Zwave Value for current temp.""" def __init__( - self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + self, config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize thermostat.""" super().__init__(config_entry, driver, info) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 7e95e274713793..3e46fc6bac3708 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -27,7 +27,6 @@ ) from homeassistant.config_entries import ( SOURCE_USB, - ConfigEntry, ConfigEntryState, ConfigFlow, ConfigFlowResult, @@ -62,11 +61,11 @@ CONF_S2_UNAUTHENTICATED_KEY, CONF_USB_PATH, CONF_USE_ADDON, - DATA_CLIENT, DOMAIN, DRIVER_READY_TIMEOUT, ) from .helpers import CannotConnect, async_get_version_info +from .models import ZwaveJSConfigEntry _LOGGER = logging.getLogger(__name__) @@ -185,7 +184,7 @@ def __init__(self) -> None: self.backup_filepath: Path | None = None self.use_addon = False self._migrating = False - self._reconfigure_config_entry: ConfigEntry | None = None + self._reconfigure_config_entry: ZwaveJSConfigEntry | None = None self._usb_discovery = False self._recommended_install = False @@ -1443,7 +1442,7 @@ def _get_driver(self) -> Driver: assert config_entry is not None if config_entry.state != ConfigEntryState.LOADED: raise AbortFlow("Configuration entry is not loaded") - client: Client = config_entry.runtime_data[DATA_CLIENT] + client: Client = config_entry.runtime_data.client assert client.driver is not None return client.driver diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index a99e9fd0113c15..6dc76ebd05d4e3 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -38,8 +38,6 @@ CONF_DATA_COLLECTION_OPTED_IN = "data_collection_opted_in" DOMAIN = "zwave_js" -DATA_CLIENT = "client" -DATA_OLD_SERVER_LOG_LEVEL = "old_server_log_level" EVENT_DEVICE_ADDED_TO_REGISTRY = f"{DOMAIN}_device_added_to_registry" EVENT_VALUE_UPDATED = "value updated" diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index dc44f46a3ce0a5..424fe94b8b9afd 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -4,7 +4,6 @@ from typing import Any, cast -from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import ( CURRENT_VALUE_PROPERTY, TARGET_STATE_PROPERTY, @@ -34,31 +33,26 @@ CoverEntity, CoverEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import ( - COVER_POSITION_PROPERTY_KEYS, - COVER_TILT_PROPERTY_KEYS, - DATA_CLIENT, - DOMAIN, -) +from .const import COVER_POSITION_PROPERTY_KEYS, COVER_TILT_PROPERTY_KEYS, DOMAIN from .discovery import ZwaveDiscoveryInfo from .discovery_data_template import CoverTiltDataTemplate from .entity import ZWaveBaseEntity +from .models import ZwaveJSConfigEntry PARALLEL_UPDATES = 0 async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ZwaveJSConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Z-Wave Cover from Config Entry.""" - client: ZwaveClient = config_entry.runtime_data[DATA_CLIENT] + client = config_entry.runtime_data.client @callback def async_add_cover(info: ZwaveDiscoveryInfo) -> None: @@ -288,7 +282,7 @@ class ZWaveMultilevelSwitchCover(CoverPositionMixin): def __init__( self, - config_entry: ConfigEntry, + config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo, ) -> None: @@ -318,7 +312,7 @@ class ZWaveTiltCover(ZWaveMultilevelSwitchCover, CoverTiltMixin): def __init__( self, - config_entry: ConfigEntry, + config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo, ) -> None: @@ -336,7 +330,7 @@ class ZWaveWindowCovering(CoverPositionMixin, CoverTiltMixin): """Representation of a Z-Wave Window Covering cover device.""" def __init__( - self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + self, config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize.""" super().__init__(config_entry, driver, info) @@ -438,7 +432,7 @@ class ZwaveMotorizedBarrier(ZWaveBaseEntity, CoverEntity): def __init__( self, - config_entry: ConfigEntry, + config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo, ) -> None: diff --git a/homeassistant/components/zwave_js/device_automation_helpers.py b/homeassistant/components/zwave_js/device_automation_helpers.py index 4eed2a5b50ca81..27c9ff2bd3483f 100644 --- a/homeassistant/components/zwave_js/device_automation_helpers.py +++ b/homeassistant/components/zwave_js/device_automation_helpers.py @@ -2,14 +2,13 @@ from __future__ import annotations -from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.model.value import ConfigurationValue from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr -from .const import DATA_CLIENT, DOMAIN +from .const import DOMAIN NODE_STATUSES = ["asleep", "awake", "dead", "alive"] @@ -55,5 +54,5 @@ def async_bypass_dynamic_config_validation(hass: HomeAssistant, device_id: str) return True # The driver may not be ready when the config entry is loaded. - client: ZwaveClient = entry.runtime_data[DATA_CLIENT] + client = entry.runtime_data.client return client.driver is None diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index 5515100b20bfb2..1929341a4be9c2 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -13,13 +13,12 @@ from zwave_js_server.util.node import dump_node_state from homeassistant.components.diagnostics import REDACTED, async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DATA_CLIENT, USER_AGENT +from .const import USER_AGENT from .helpers import ( ZwaveValueMatcher, get_home_and_node_id_from_device_entry, @@ -27,6 +26,7 @@ get_value_id_from_unique_id, value_matches_matcher, ) +from .models import ZwaveJSConfigEntry KEYS_TO_REDACT = {"homeId", "location"} @@ -73,7 +73,10 @@ def redact_node_state(node_state: dict) -> dict: def get_device_entities( - hass: HomeAssistant, node: Node, config_entry: ConfigEntry, device: dr.DeviceEntry + hass: HomeAssistant, + node: Node, + config_entry: ZwaveJSConfigEntry, + device: dr.DeviceEntry, ) -> list[dict[str, Any]]: """Get entities for a device.""" entity_entries = er.async_entries_for_device( @@ -125,7 +128,7 @@ def get_device_entities( async def async_get_config_entry_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: ZwaveJSConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" msgs: list[dict] = async_redact_data( @@ -144,10 +147,10 @@ async def async_get_config_entry_diagnostics( async def async_get_device_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry, device: dr.DeviceEntry + hass: HomeAssistant, config_entry: ZwaveJSConfigEntry, device: dr.DeviceEntry ) -> dict[str, Any]: """Return diagnostics for a device.""" - client: Client = config_entry.runtime_data[DATA_CLIENT] + client: Client = config_entry.runtime_data.client identifiers = get_home_and_node_id_from_device_entry(device) node_id = identifiers[1] if identifiers else None driver = client.driver diff --git a/homeassistant/components/zwave_js/event.py b/homeassistant/components/zwave_js/event.py index 66959aa9b7536c..60f0e1101089cd 100644 --- a/homeassistant/components/zwave_js/event.py +++ b/homeassistant/components/zwave_js/event.py @@ -2,30 +2,29 @@ from __future__ import annotations -from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.model.driver import Driver from zwave_js_server.model.value import Value, ValueNotification from homeassistant.components.event import DOMAIN as EVENT_DOMAIN, EventEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import ATTR_VALUE, DATA_CLIENT, DOMAIN +from .const import ATTR_VALUE, DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity +from .models import ZwaveJSConfigEntry PARALLEL_UPDATES = 0 async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ZwaveJSConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Z-Wave Event entity from Config Entry.""" - client: ZwaveClient = config_entry.runtime_data[DATA_CLIENT] + client = config_entry.runtime_data.client @callback def async_add_event(info: ZwaveDiscoveryInfo) -> None: @@ -56,7 +55,7 @@ class ZwaveEventEntity(ZWaveBaseEntity, EventEntity): """Representation of a Z-Wave event entity.""" def __init__( - self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + self, config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize a ZwaveEventEntity entity.""" super().__init__(config_entry, driver, info) diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index ae36e0afb42e84..8e47cbbeb1ddbc 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -5,7 +5,6 @@ import math from typing import Any, cast -from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import TARGET_VALUE_PROPERTY, CommandClass from zwave_js_server.const.command_class.multilevel_switch import SET_TO_PREVIOUS_VALUE from zwave_js_server.const.command_class.thermostat import ( @@ -20,7 +19,6 @@ FanEntity, FanEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -30,11 +28,12 @@ ranged_value_to_percentage, ) -from .const import DATA_CLIENT, DOMAIN +from .const import DOMAIN from .discovery import ZwaveDiscoveryInfo from .discovery_data_template import FanValueMapping, FanValueMappingDataTemplate from .entity import ZWaveBaseEntity from .helpers import get_value_of_zwave_value +from .models import ZwaveJSConfigEntry PARALLEL_UPDATES = 0 @@ -45,11 +44,11 @@ async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ZwaveJSConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Z-Wave Fan from Config Entry.""" - client: ZwaveClient = config_entry.runtime_data[DATA_CLIENT] + client = config_entry.runtime_data.client @callback def async_add_fan(info: ZwaveDiscoveryInfo) -> None: @@ -85,7 +84,7 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity): ) def __init__( - self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + self, config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize the fan.""" super().__init__(config_entry, driver, info) @@ -165,7 +164,7 @@ class ValueMappingZwaveFan(ZwaveFan): """A Zwave fan with a value mapping data (e.g., 1-24 is low).""" def __init__( - self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + self, config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize the fan.""" super().__init__(config_entry, driver, info) @@ -316,7 +315,7 @@ class ZwaveThermostatFan(ZWaveBaseEntity, FanEntity): _fan_state: ZwaveValue | None = None def __init__( - self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + self, config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize the thermostat fan.""" super().__init__(config_entry, driver, info) diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index bfa093f7db9506..5694be5482bc5e 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -10,7 +10,6 @@ import aiohttp import voluptuous as vol -from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import ( LOG_LEVEL_MAP, CommandClass, @@ -30,7 +29,7 @@ from zwave_js_server.version import VersionInfo, get_server_version from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_AREA_ID, ATTR_DEVICE_ID, @@ -51,12 +50,11 @@ ATTR_ENDPOINT, ATTR_PROPERTY, ATTR_PROPERTY_KEY, - DATA_CLIENT, - DATA_OLD_SERVER_LOG_LEVEL, DOMAIN, LIB_LOGGER, LOGGER, ) +from .models import ZwaveJSConfigEntry SERVER_VERSION_TIMEOUT = 10 @@ -143,7 +141,7 @@ async def async_enable_statistics(driver: Driver) -> None: async def async_enable_server_logging_if_needed( - hass: HomeAssistant, entry: ConfigEntry, driver: Driver + hass: HomeAssistant, entry: ZwaveJSConfigEntry, driver: Driver ) -> None: """Enable logging of zwave-js-server in the lib.""" # If lib log level is set to debug, we want to enable server logging. First we @@ -161,15 +159,14 @@ async def async_enable_server_logging_if_needed( if (curr_server_log_level := driver.log_config.level) and ( LOG_LEVEL_MAP[curr_server_log_level] ) > LIB_LOGGER.getEffectiveLevel(): - entry_data = entry.runtime_data - entry_data[DATA_OLD_SERVER_LOG_LEVEL] = curr_server_log_level + entry.runtime_data.old_server_log_level = curr_server_log_level await driver.async_update_log_config(LogConfig(level=LogLevel.DEBUG)) await driver.client.enable_server_logging() LOGGER.info("Zwave-js-server logging is enabled") async def async_disable_server_logging_if_needed( - hass: HomeAssistant, entry: ConfigEntry, driver: Driver + hass: HomeAssistant, entry: ZwaveJSConfigEntry, driver: Driver ) -> None: """Disable logging of zwave-js-server in the lib if still connected to server.""" if ( @@ -180,10 +177,8 @@ async def async_disable_server_logging_if_needed( return LOGGER.info("Disabling zwave_js server logging") if ( - DATA_OLD_SERVER_LOG_LEVEL in entry.runtime_data - and (old_server_log_level := entry.runtime_data.pop(DATA_OLD_SERVER_LOG_LEVEL)) - != driver.log_config.level - ): + old_server_log_level := entry.runtime_data.old_server_log_level + ) is not None and old_server_log_level != driver.log_config.level: LOGGER.info( ( "Server logging is currently set to %s as a result of server logging " @@ -193,6 +188,7 @@ async def async_disable_server_logging_if_needed( old_server_log_level, ) await driver.async_update_log_config(LogConfig(level=old_server_log_level)) + entry.runtime_data.old_server_log_level = None driver.client.disable_server_logging() LOGGER.info("Zwave-js-server logging is enabled") @@ -262,7 +258,7 @@ def async_get_node_from_device_id( # Use device config entry ID's to validate that this is a valid zwave_js device # and to get the client config_entry_ids = device_entry.config_entries - entry = next( + entry: ZwaveJSConfigEntry | None = next( ( entry for entry in hass.config_entries.async_entries(DOMAIN) @@ -277,7 +273,7 @@ def async_get_node_from_device_id( if entry.state != ConfigEntryState.LOADED: raise ValueError(f"Device {device_id} config entry is not loaded") - client: ZwaveClient = entry.runtime_data[DATA_CLIENT] + client = entry.runtime_data.client driver = client.driver if driver is None: @@ -310,7 +306,7 @@ async def async_get_provisioning_entry_from_device_id( # Use device config entry ID's to validate that this is a valid zwave_js device # and to get the client config_entry_ids = device_entry.config_entries - entry = next( + entry: ZwaveJSConfigEntry | None = next( ( entry for entry in hass.config_entries.async_entries(DOMAIN) @@ -325,7 +321,7 @@ async def async_get_provisioning_entry_from_device_id( if entry.state != ConfigEntryState.LOADED: raise ValueError(f"Device {device_id} config entry is not loaded") - client: ZwaveClient = entry.runtime_data[DATA_CLIENT] + client = entry.runtime_data.client driver = client.driver if driver is None: @@ -393,7 +389,7 @@ def async_get_nodes_from_area_id( for device in dr.async_entries_for_area(dev_reg, area_id) if any( cast( - ConfigEntry, + ZwaveJSConfigEntry, hass.config_entries.async_get_entry(config_entry_id), ).domain == DOMAIN @@ -487,7 +483,7 @@ def async_get_node_status_sensor_entity_id( entry = hass.config_entries.async_get_entry(entry_id) assert entry - client = entry.runtime_data[DATA_CLIENT] + client = entry.runtime_data.client node = async_get_node_from_device_id(hass, device_id, dev_reg) return ent_reg.async_get_entity_id( SENSOR_DOMAIN, @@ -565,7 +561,7 @@ def get_device_info(driver: Driver, node: ZwaveNode) -> DeviceInfo: def get_network_identifier_for_notification( - hass: HomeAssistant, config_entry: ConfigEntry, controller: Controller + hass: HomeAssistant, config_entry: ZwaveJSConfigEntry, controller: Controller ) -> str: """Return the network identifier string for persistent notifications.""" home_id = str(controller.home_id) diff --git a/homeassistant/components/zwave_js/humidifier.py b/homeassistant/components/zwave_js/humidifier.py index 2b85bd4449f188..83f5e507c01948 100644 --- a/homeassistant/components/zwave_js/humidifier.py +++ b/homeassistant/components/zwave_js/humidifier.py @@ -5,7 +5,6 @@ from dataclasses import dataclass from typing import Any -from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import CommandClass from zwave_js_server.const.command_class.humidity_control import ( HUMIDITY_CONTROL_SETPOINT_PROPERTY, @@ -23,14 +22,14 @@ HumidifierEntity, HumidifierEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DATA_CLIENT, DOMAIN +from .const import DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity +from .models import ZwaveJSConfigEntry PARALLEL_UPDATES = 0 @@ -69,11 +68,11 @@ class ZwaveHumidifierEntityDescription(HumidifierEntityDescription): async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ZwaveJSConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Z-Wave humidifier from config entry.""" - client: ZwaveClient = config_entry.runtime_data[DATA_CLIENT] + client = config_entry.runtime_data.client @callback def async_add_humidifier(info: ZwaveDiscoveryInfo) -> None: @@ -122,7 +121,7 @@ class ZWaveHumidifier(ZWaveBaseEntity, HumidifierEntity): def __init__( self, - config_entry: ConfigEntry, + config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo, description: ZwaveHumidifierEntityDescription, diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index f60e129cc77df9..23ec240e5a75ac 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -4,7 +4,6 @@ from typing import TYPE_CHECKING, Any, cast -from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import ( TARGET_VALUE_PROPERTY, TRANSITION_DURATION_OPTION, @@ -38,15 +37,15 @@ LightEntity, LightEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util import color as color_util -from .const import DATA_CLIENT, DOMAIN +from .const import DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity +from .models import ZwaveJSConfigEntry PARALLEL_UPDATES = 0 @@ -66,11 +65,11 @@ async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ZwaveJSConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Z-Wave Light from Config Entry.""" - client: ZwaveClient = config_entry.runtime_data[DATA_CLIENT] + client = config_entry.runtime_data.client @callback def async_add_light(info: ZwaveDiscoveryInfo) -> None: @@ -109,7 +108,7 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): _attr_max_color_temp_kelvin = 6500 # 153 mireds as a safe default def __init__( - self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + self, config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize the light.""" super().__init__(config_entry, driver, info) @@ -539,7 +538,7 @@ class ZwaveColorOnOffLight(ZwaveLight): """ def __init__( - self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + self, config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize the light.""" super().__init__(config_entry, driver, info) diff --git a/homeassistant/components/zwave_js/lock.py b/homeassistant/components/zwave_js/lock.py index f609084955c68a..6e22afd3d2d722 100644 --- a/homeassistant/components/zwave_js/lock.py +++ b/homeassistant/components/zwave_js/lock.py @@ -5,7 +5,6 @@ from typing import Any import voluptuous as vol -from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import CommandClass from zwave_js_server.const.command_class.lock import ( ATTR_CODE_SLOT, @@ -20,7 +19,6 @@ from zwave_js_server.util.lock import clear_usercode, set_configuration, set_usercode from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockEntity, LockState -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_platform @@ -34,7 +32,6 @@ ATTR_LOCK_TIMEOUT, ATTR_OPERATION_TYPE, ATTR_TWIST_ASSIST, - DATA_CLIENT, DOMAIN, LOGGER, SERVICE_CLEAR_LOCK_USERCODE, @@ -43,6 +40,7 @@ ) from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity +from .models import ZwaveJSConfigEntry PARALLEL_UPDATES = 0 @@ -61,11 +59,11 @@ async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ZwaveJSConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Z-Wave lock from config entry.""" - client: ZwaveClient = config_entry.runtime_data[DATA_CLIENT] + client = config_entry.runtime_data.client @callback def async_add_lock(info: ZwaveDiscoveryInfo) -> None: diff --git a/homeassistant/components/zwave_js/models.py b/homeassistant/components/zwave_js/models.py new file mode 100644 index 00000000000000..63f77871c141dc --- /dev/null +++ b/homeassistant/components/zwave_js/models.py @@ -0,0 +1,27 @@ +"""Type definitions for Z-Wave JS integration.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from zwave_js_server.const import LogLevel + +from homeassistant.config_entries import ConfigEntry + +if TYPE_CHECKING: + from zwave_js_server.client import Client as ZwaveClient + + from . import DriverEvents + + +@dataclass +class ZwaveJSData: + """Data for zwave_js runtime data.""" + + client: ZwaveClient + driver_events: DriverEvents + old_server_log_level: LogLevel | None = None + + +type ZwaveJSConfigEntry = ConfigEntry[ZwaveJSData] diff --git a/homeassistant/components/zwave_js/number.py b/homeassistant/components/zwave_js/number.py index 2e2d93bbdbe30a..982966ce3a9401 100644 --- a/homeassistant/components/zwave_js/number.py +++ b/homeassistant/components/zwave_js/number.py @@ -5,33 +5,32 @@ from collections.abc import Mapping from typing import Any, cast -from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import TARGET_VALUE_PROPERTY from zwave_js_server.model.driver import Driver from zwave_js_server.model.value import Value from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import ATTR_RESERVED_VALUES, DATA_CLIENT, DOMAIN +from .const import ATTR_RESERVED_VALUES, DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity +from .models import ZwaveJSConfigEntry PARALLEL_UPDATES = 0 async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ZwaveJSConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Z-Wave Number entity from Config Entry.""" - client: ZwaveClient = config_entry.runtime_data[DATA_CLIENT] + client = config_entry.runtime_data.client @callback def async_add_number(info: ZwaveDiscoveryInfo) -> None: @@ -62,7 +61,7 @@ class ZwaveNumberEntity(ZWaveBaseEntity, NumberEntity): """Representation of a Z-Wave number entity.""" def __init__( - self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + self, config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize a ZwaveNumberEntity entity.""" super().__init__(config_entry, driver, info) @@ -114,7 +113,7 @@ class ZWaveConfigParameterNumberEntity(ZwaveNumberEntity): _attr_entity_category = EntityCategory.CONFIG def __init__( - self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + self, config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize a ZWaveConfigParameterNumber entity.""" super().__init__(config_entry, driver, info) @@ -142,7 +141,7 @@ class ZwaveVolumeNumberEntity(ZWaveBaseEntity, NumberEntity): """Representation of a volume number entity.""" def __init__( - self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + self, config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize a ZwaveVolumeNumberEntity entity.""" super().__init__(config_entry, driver, info) diff --git a/homeassistant/components/zwave_js/select.py b/homeassistant/components/zwave_js/select.py index 8a6ccc57c1784a..b8c84d02c95caf 100644 --- a/homeassistant/components/zwave_js/select.py +++ b/homeassistant/components/zwave_js/select.py @@ -4,33 +4,32 @@ from typing import cast -from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import TARGET_VALUE_PROPERTY, CommandClass from zwave_js_server.const.command_class.lock import TARGET_MODE_PROPERTY from zwave_js_server.const.command_class.sound_switch import TONE_ID_PROPERTY, ToneID from zwave_js_server.model.driver import Driver from homeassistant.components.select import DOMAIN as SELECT_DOMAIN, SelectEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DATA_CLIENT, DOMAIN +from .const import DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity +from .models import ZwaveJSConfigEntry PARALLEL_UPDATES = 0 async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ZwaveJSConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Z-Wave Select entity from Config Entry.""" - client: ZwaveClient = config_entry.runtime_data[DATA_CLIENT] + client = config_entry.runtime_data.client @callback def async_add_select(info: ZwaveDiscoveryInfo) -> None: @@ -69,7 +68,7 @@ class ZwaveSelectEntity(ZWaveBaseEntity, SelectEntity): _attr_entity_category = EntityCategory.CONFIG def __init__( - self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + self, config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize a ZwaveSelectEntity entity.""" super().__init__(config_entry, driver, info) @@ -103,7 +102,7 @@ class ZWaveDoorLockSelectEntity(ZwaveSelectEntity): """Representation of a Z-Wave door lock CC mode select entity.""" def __init__( - self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + self, config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize a ZWaveDoorLockSelectEntity entity.""" super().__init__(config_entry, driver, info) @@ -126,7 +125,7 @@ class ZWaveConfigParameterSelectEntity(ZwaveSelectEntity): _attr_entity_category = EntityCategory.CONFIG def __init__( - self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + self, config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize a ZWaveConfigParameterSelect entity.""" super().__init__(config_entry, driver, info) @@ -145,7 +144,7 @@ class ZwaveDefaultToneSelectEntity(ZWaveBaseEntity, SelectEntity): _attr_entity_category = EntityCategory.CONFIG def __init__( - self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + self, config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize a ZwaveDefaultToneSelectEntity entity.""" super().__init__(config_entry, driver, info) @@ -194,7 +193,7 @@ class ZwaveMultilevelSwitchSelectEntity(ZWaveBaseEntity, SelectEntity): """Representation of a Z-Wave Multilevel Switch CC select entity.""" def __init__( - self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + self, config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize a ZwaveSelectEntity entity.""" super().__init__(config_entry, driver, info) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 05fa785760bab7..ac65b9e2749b08 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -7,7 +7,6 @@ from typing import Any import voluptuous as vol -from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import CommandClass from zwave_js_server.const.command_class.meter import ( RESET_METER_OPTION_TARGET_VALUE, @@ -28,7 +27,6 @@ SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, LIGHT_LUX, @@ -56,7 +54,6 @@ ATTR_METER_TYPE, ATTR_METER_TYPE_NAME, ATTR_VALUE, - DATA_CLIENT, DOMAIN, ENTITY_DESC_KEY_BATTERY_LEVEL, ENTITY_DESC_KEY_BATTERY_LIST_STATE, @@ -94,6 +91,7 @@ from .entity import ZWaveBaseEntity from .helpers import get_device_info, get_valueless_base_unique_id from .migrate import async_migrate_statistics_sensors +from .models import ZwaveJSConfigEntry PARALLEL_UPDATES = 0 @@ -576,11 +574,11 @@ def get_entity_description( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ZwaveJSConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Z-Wave sensor from config entry.""" - client: ZwaveClient = config_entry.runtime_data[DATA_CLIENT] + client = config_entry.runtime_data.client driver = client.driver assert driver is not None # Driver is ready before platforms are loaded. @@ -717,7 +715,7 @@ class ZwaveSensor(ZWaveBaseEntity, SensorEntity): def __init__( self, - config_entry: ConfigEntry, + config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo, entity_description: SensorEntityDescription, @@ -756,7 +754,7 @@ class ZWaveNumericSensor(ZwaveSensor): def __init__( self, - config_entry: ConfigEntry, + config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo, entity_description: SensorEntityDescription, @@ -831,7 +829,7 @@ class ZWaveListSensor(ZwaveSensor): def __init__( self, - config_entry: ConfigEntry, + config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo, entity_description: SensorEntityDescription, @@ -870,7 +868,7 @@ class ZWaveConfigParameterSensor(ZWaveListSensor): def __init__( self, - config_entry: ConfigEntry, + config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo, entity_description: SensorEntityDescription, @@ -906,7 +904,7 @@ class ZWaveNodeStatusSensor(SensorEntity): _attr_translation_key = "node_status" def __init__( - self, config_entry: ConfigEntry, driver: Driver, node: ZwaveNode + self, config_entry: ZwaveJSConfigEntry, driver: Driver, node: ZwaveNode ) -> None: """Initialize a generic Z-Wave device entity.""" self.config_entry = config_entry @@ -968,7 +966,7 @@ class ZWaveControllerStatusSensor(SensorEntity): _attr_has_entity_name = True _attr_translation_key = "controller_status" - def __init__(self, config_entry: ConfigEntry, driver: Driver) -> None: + def __init__(self, config_entry: ZwaveJSConfigEntry, driver: Driver) -> None: """Initialize a generic Z-Wave device entity.""" self.config_entry = config_entry self.controller = driver.controller @@ -1030,7 +1028,7 @@ class ZWaveStatisticsSensor(SensorEntity): def __init__( self, - config_entry: ConfigEntry, + config_entry: ZwaveJSConfigEntry, driver: Driver, statistics_src: ZwaveNode | Controller, description: ZWaveJSStatisticsSensorEntityDescription, diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 076e3b6a50d302..9420159b806031 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -704,7 +704,7 @@ async def async_multicast_set_value(self, service: ServiceCall) -> None: client = first_node.client except StopIteration: data = self._hass.config_entries.async_entries(const.DOMAIN)[0].runtime_data - client = data[const.DATA_CLIENT] + client = data.client assert client.driver first_node = next( node diff --git a/homeassistant/components/zwave_js/siren.py b/homeassistant/components/zwave_js/siren.py index f0526171a702de..f63a3bb914467c 100644 --- a/homeassistant/components/zwave_js/siren.py +++ b/homeassistant/components/zwave_js/siren.py @@ -4,7 +4,6 @@ from typing import Any -from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const.command_class.sound_switch import ToneID from zwave_js_server.model.driver import Driver @@ -15,25 +14,25 @@ SirenEntity, SirenEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DATA_CLIENT, DOMAIN +from .const import DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity +from .models import ZwaveJSConfigEntry PARALLEL_UPDATES = 0 async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ZwaveJSConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Z-Wave Siren entity from Config Entry.""" - client: ZwaveClient = config_entry.runtime_data[DATA_CLIENT] + client = config_entry.runtime_data.client @callback def async_add_siren(info: ZwaveDiscoveryInfo) -> None: @@ -57,7 +56,7 @@ class ZwaveSirenEntity(ZWaveBaseEntity, SirenEntity): """Representation of a Z-Wave siren entity.""" def __init__( - self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + self, config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize a ZwaveSirenEntity entity.""" super().__init__(config_entry, driver, info) diff --git a/homeassistant/components/zwave_js/switch.py b/homeassistant/components/zwave_js/switch.py index 2ff80d8505e66b..75e6b31bc500c7 100644 --- a/homeassistant/components/zwave_js/switch.py +++ b/homeassistant/components/zwave_js/switch.py @@ -4,7 +4,6 @@ from typing import Any -from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import TARGET_VALUE_PROPERTY from zwave_js_server.const.command_class.barrier_operator import ( BarrierEventSignalingSubsystemState, @@ -12,26 +11,26 @@ from zwave_js_server.model.driver import Driver from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DATA_CLIENT, DOMAIN +from .const import DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity +from .models import ZwaveJSConfigEntry PARALLEL_UPDATES = 0 async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ZwaveJSConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Z-Wave sensor from config entry.""" - client: ZwaveClient = config_entry.runtime_data[DATA_CLIENT] + client = config_entry.runtime_data.client @callback def async_add_switch(info: ZwaveDiscoveryInfo) -> None: @@ -65,7 +64,7 @@ class ZWaveSwitch(ZWaveBaseEntity, SwitchEntity): """Representation of a Z-Wave switch.""" def __init__( - self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + self, config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize the switch.""" super().__init__(config_entry, driver, info) @@ -95,7 +94,7 @@ class ZWaveIndicatorSwitch(ZWaveSwitch): """Representation of a Z-Wave Indicator CC switch.""" def __init__( - self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + self, config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize the switch.""" super().__init__(config_entry, driver, info) @@ -108,7 +107,7 @@ class ZWaveBarrierEventSignalingSwitch(ZWaveBaseEntity, SwitchEntity): def __init__( self, - config_entry: ConfigEntry, + config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo, ) -> None: @@ -164,7 +163,7 @@ class ZWaveConfigParameterSwitch(ZWaveSwitch): _attr_entity_category = EntityCategory.CONFIG def __init__( - self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo + self, config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize a ZWaveConfigParameterSwitch entity.""" super().__init__(config_entry, driver, info) diff --git a/homeassistant/components/zwave_js/triggers/event.py b/homeassistant/components/zwave_js/triggers/event.py index f74357327e9f79..8d0ccf60fdfd03 100644 --- a/homeassistant/components/zwave_js/triggers/event.py +++ b/homeassistant/components/zwave_js/triggers/event.py @@ -7,7 +7,6 @@ from pydantic.v1 import ValidationError import voluptuous as vol -from zwave_js_server.client import Client from zwave_js_server.model.controller import CONTROLLER_EVENT_MODEL_MAP from zwave_js_server.model.driver import DRIVER_EVENT_MODEL_MAP, Driver from zwave_js_server.model.node import NODE_EVENT_MODEL_MAP @@ -26,7 +25,6 @@ ATTR_EVENT_SOURCE, ATTR_NODE_ID, ATTR_PARTIAL_DICT_MATCH, - DATA_CLIENT, DOMAIN, ) from ..helpers import ( @@ -219,7 +217,7 @@ def _create_zwave_listeners() -> None: entry_id = config[ATTR_CONFIG_ENTRY_ID] entry = hass.config_entries.async_get_entry(entry_id) assert entry - client: Client = entry.runtime_data[DATA_CLIENT] + client = entry.runtime_data.client driver = client.driver assert driver drivers.add(driver) diff --git a/homeassistant/components/zwave_js/triggers/trigger_helpers.py b/homeassistant/components/zwave_js/triggers/trigger_helpers.py index 1ef9ebaae28011..917d207109f8aa 100644 --- a/homeassistant/components/zwave_js/triggers/trigger_helpers.py +++ b/homeassistant/components/zwave_js/triggers/trigger_helpers.py @@ -1,14 +1,12 @@ """Helpers for Z-Wave JS custom triggers.""" -from zwave_js_server.client import Client as ZwaveClient - from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.typing import ConfigType -from ..const import ATTR_CONFIG_ENTRY_ID, DATA_CLIENT, DOMAIN +from ..const import ATTR_CONFIG_ENTRY_ID, DOMAIN @callback @@ -37,7 +35,7 @@ def async_bypass_dynamic_config_validation( return True # The driver may not be ready when the config entry is loaded. - client: ZwaveClient = entry.runtime_data[DATA_CLIENT] + client = entry.runtime_data.client if client.driver is None: return True diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 985c4a86813008..4355857f5df9ce 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -10,7 +10,6 @@ from typing import Any, Final from awesomeversion import AwesomeVersion -from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import NodeStatus from zwave_js_server.exceptions import BaseZwaveJSServerError, FailedZWaveCommand from zwave_js_server.model.driver import Driver @@ -27,7 +26,6 @@ UpdateEntity, UpdateEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -36,8 +34,9 @@ from homeassistant.helpers.event import async_call_later from homeassistant.helpers.restore_state import ExtraStoredData -from .const import API_KEY_FIRMWARE_UPDATE_SERVICE, DATA_CLIENT, DOMAIN, LOGGER +from .const import API_KEY_FIRMWARE_UPDATE_SERVICE, DOMAIN, LOGGER from .helpers import get_device_info, get_valueless_base_unique_id +from .models import ZwaveJSConfigEntry PARALLEL_UPDATES = 1 @@ -76,11 +75,11 @@ def from_dict(cls, data: dict[str, Any]) -> ZWaveNodeFirmwareUpdateExtraStoredDa async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: ZwaveJSConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Z-Wave update entity from config entry.""" - client: ZwaveClient = config_entry.runtime_data[DATA_CLIENT] + client = config_entry.runtime_data.client cnt: Counter = Counter() @callback