From f5312d4444406dca43b2315a51d119440080feed Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 3 Aug 2026 23:39:44 +0200 Subject: [PATCH 1/5] Make switch non-optimistic for Mikrotik (#178113) --- homeassistant/components/mikrotik/switch.py | 3 +- tests/components/mikrotik/test_switch.py | 79 +++++++++++++-------- 2 files changed, 51 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/mikrotik/switch.py b/homeassistant/components/mikrotik/switch.py index 44ad66a92e3c4..99da0d5f5aacf 100644 --- a/homeassistant/components/mikrotik/switch.py +++ b/homeassistant/components/mikrotik/switch.py @@ -69,8 +69,7 @@ async def _set_state(self, action: str) -> None: f"/interface/{action}", {".id": self._interface[".id"]}, ) - self._interface["disabled"] = action == "disable" - self.async_write_ha_state() + await self.coordinator.async_request_refresh() @override async def async_turn_on(self, **kwargs: Any) -> None: diff --git a/tests/components/mikrotik/test_switch.py b/tests/components/mikrotik/test_switch.py index c2ccd7ccd128c..c2f93374f02f4 100644 --- a/tests/components/mikrotik/test_switch.py +++ b/tests/components/mikrotik/test_switch.py @@ -1,7 +1,9 @@ """Tests for the Mikrotik switch platform.""" +from typing import Any from unittest.mock import MagicMock, patch +import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.switch import ( @@ -39,45 +41,64 @@ async def test_switch_no_matching_interfaces(hass: HomeAssistant) -> None: assert hass.states.async_entity_ids(SWITCH_DOMAIN) == [] -async def test_switch_turn_on(hass: HomeAssistant, mock_api: MagicMock) -> None: - """Test turning on a Mikrotik switch enables the interface.""" - with patch("homeassistant.components.mikrotik.PLATFORMS", [Platform.SWITCH]): - await setup_mikrotik_entry(hass, interface_data=[dict(WLAN1_INTERFACE)]) - - entity_id = "switch.wlan1_wlan" - assert (state := hass.states.get(entity_id)) - assert state.state == STATE_OFF - - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - - mock_api.assert_called_with("/interface/enable", **{".id": "*2"}) - - assert (state := hass.states.get(entity_id)) - assert state.state == STATE_ON - +@pytest.mark.parametrize( + ( + "interface", + "entity_id", + "initial_state", + "service", + "command", + "final_state", + ), + [ + pytest.param( + ETHER1_INTERFACE, + "switch.ether1_ethernet", + STATE_ON, + SERVICE_TURN_OFF, + "/interface/disable", + STATE_OFF, + id="turn_off", + ), + pytest.param( + WLAN1_INTERFACE, + "switch.wlan1_wlan", + STATE_OFF, + SERVICE_TURN_ON, + "/interface/enable", + STATE_ON, + id="turn_on", + ), + ], +) +async def test_switch_turn_on_off( + hass: HomeAssistant, + mock_api: MagicMock, + interface: dict[str, Any], + entity_id: str, + initial_state: str, + service: str, + command: str, + final_state: str, +) -> None: + """Test turning a Mikrotik switch on/off updates state via the coordinator.""" -async def test_switch_turn_off(hass: HomeAssistant, mock_api: MagicMock) -> None: - """Test turning off a Mikrotik switch disables the interface.""" with patch("homeassistant.components.mikrotik.PLATFORMS", [Platform.SWITCH]): - await setup_mikrotik_entry(hass, interface_data=[dict(ETHER1_INTERFACE)]) + await setup_mikrotik_entry(hass, interface_data=[interface]) - entity_id = "switch.ether1_ethernet" assert (state := hass.states.get(entity_id)) - assert state.state == STATE_ON + assert state.state == initial_state + + mock_api.return_value = [{**interface, "disabled": final_state == STATE_OFF}] await hass.services.async_call( SWITCH_DOMAIN, - SERVICE_TURN_OFF, + service, {ATTR_ENTITY_ID: entity_id}, blocking=True, ) - mock_api.assert_called_with("/interface/disable", **{".id": "*1"}) + mock_api.assert_any_call(command, **{".id": interface[".id"]}) assert (state := hass.states.get(entity_id)) - assert state.state == STATE_OFF + assert state.state == final_state From 72f68f2f4e82826f4f5db90bff12c50984a07706 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 4 Aug 2026 00:44:32 +0200 Subject: [PATCH 2/5] Improve DeviceInfo for Midea (#177771) --- homeassistant/components/midea/entity.py | 16 +++++--- tests/components/midea/conftest.py | 6 ++- tests/components/midea/const.py | 2 + tests/components/midea/test_entity.py | 51 +++++++++++++++++++++++- 4 files changed, 68 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/midea/entity.py b/homeassistant/components/midea/entity.py index db59109622ca4..02a612592425f 100644 --- a/homeassistant/components/midea/entity.py +++ b/homeassistant/components/midea/entity.py @@ -5,7 +5,7 @@ from midealocal.device import MideaDevice from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.entity import Entity, EntityDescription from .const import DOMAIN, LOGGER @@ -43,15 +43,21 @@ async def async_will_remove_from_hass(self) -> None: @override def device_info(self) -> DeviceInfo: """Return device info.""" - return DeviceInfo( + device_info = DeviceInfo( manufacturer="Midea", # Map the device type (numeric ID) to a human-readable model name. - model=MIDEA_DEVICE_NAMES.get(self._device.device_type, "Unknown"), + model=MIDEA_DEVICE_NAMES.get(self._device.device_type), identifiers={(DOMAIN, str(self._device.device_id))}, name=self._device_name, - model_id=str(self._device.device_type), - hw_version=str(self._device.subtype), + model_id=self._device.device_type.name, + hw_version=str(self._device.model), ) + if mac := self._device.mac: + device_info["connections"] = {(CONNECTION_NETWORK_MAC, mac)} + if serial_number := self._device.serial_number: + device_info["serial_number"] = serial_number + + return device_info @property @override diff --git a/tests/components/midea/conftest.py b/tests/components/midea/conftest.py index bdb1522c87386..8c2e917fb96d1 100644 --- a/tests/components/midea/conftest.py +++ b/tests/components/midea/conftest.py @@ -16,8 +16,10 @@ BASE_DATA, TEST_DEVICE_ID, TEST_KEY, + TEST_MAC_ADDRESS, TEST_MODEL, TEST_NAME, + TEST_SERIAL_NUMBER, TEST_SUBTYPE, TEST_TOKEN, ) @@ -30,7 +32,7 @@ class DummyDevice: def __init__( self, - device_type: int, + device_type: DeviceType, *, attributes: dict | None = None, ) -> None: @@ -57,6 +59,8 @@ def __init__( "Fast-heating", "Standby", ] + self.mac = TEST_MAC_ADDRESS + self.serial_number = TEST_SERIAL_NUMBER def register_update(self, callback: Callable) -> None: """Record update callback registration.""" diff --git a/tests/components/midea/const.py b/tests/components/midea/const.py index 4aa98748679b8..221708d7dbf5c 100644 --- a/tests/components/midea/const.py +++ b/tests/components/midea/const.py @@ -17,11 +17,13 @@ TEST_DEVICE_ID = 12345678 TEST_IP_ADDRESS = "1.1.1.1" TEST_KEY = "bb" * 16 +TEST_MAC_ADDRESS = "00:11:22:33:44:55" TEST_MODEL = "MSAGBU-09HRFN8" TEST_NAME = "Bedroom AC" TEST_PORT = 6444 TEST_PROTOCOL = ProtocolVersion.V3 TEST_SUBTYPE = 0 +TEST_SERIAL_NUMBER = "1234567890" TEST_TOKEN = "aa" * 16 TEST_TYPE = next(iter(MIDEA_DEVICE_NAMES)) diff --git a/tests/components/midea/test_entity.py b/tests/components/midea/test_entity.py index 2289a43b89c6d..26347d9732a1c 100644 --- a/tests/components/midea/test_entity.py +++ b/tests/components/midea/test_entity.py @@ -5,11 +5,13 @@ from midealocal.devices.ac import DeviceAttributes as ACAttributes import pytest +from homeassistant.components.midea.const import DOMAIN from homeassistant.core import CoreState, HomeAssistant +from homeassistant.helpers import device_registry as dr from . import setup_integration from .conftest import DummyDevice, default_ac_device, entity_entries -from .const import TEST_DEVICE_ID +from .const import TEST_DEVICE_ID, TEST_MAC_ADDRESS, TEST_MODEL, TEST_SERIAL_NUMBER from tests.common import MockConfigEntry @@ -79,6 +81,53 @@ async def test_entity_updates_from_device_callback( assert (state.state == "unavailable") is expected_unavailable +@pytest.mark.parametrize( + ("mac", "serial_number", "expected_connections", "expected_serial_number"), + [ + pytest.param( + TEST_MAC_ADDRESS, + TEST_SERIAL_NUMBER, + {(dr.CONNECTION_NETWORK_MAC, TEST_MAC_ADDRESS)}, + TEST_SERIAL_NUMBER, + id="populated", + ), + pytest.param( + "", + "", + set(), + None, + id="absent", + ), + ], +) +async def test_device_info_optional_metadata( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_config_entry: Callable[[DummyDevice], MockConfigEntry], + mac: str, + serial_number: str, + expected_connections: set[tuple[str, str]], + expected_serial_number: str | None, +) -> None: + """Test device registry entry reflects optional mac and serial number.""" + device = default_ac_device() + device.mac = mac + device.serial_number = serial_number + config_entry = mock_config_entry(device) + await setup_integration(hass, config_entry, device) + + assert ( + device_entry := device_registry.async_get_device( + identifiers={(DOMAIN, str(TEST_DEVICE_ID))} + ) + ) is not None + + assert device_entry.model_id == device.device_type.name + assert device_entry.hw_version == TEST_MODEL + assert device_entry.connections == expected_connections + assert device_entry.serial_number == expected_serial_number + + async def test_entity_callback_ignored_while_hass_stopping( hass: HomeAssistant, mock_config_entry: Callable[[DummyDevice], MockConfigEntry], From 1ee283df5b5fcf886c8f488cb9354aa1e6ae869a Mon Sep 17 00:00:00 2001 From: Sankalp Thakur <31366524+sankalpsthakur@users.noreply.github.com> Date: Tue, 4 Aug 2026 04:17:35 +0530 Subject: [PATCH 3/5] Migrate google_mail off hass.data[DOMAIN] to entry.runtime_data / typed storage (#177949) --- homeassistant/components/google_mail/__init__.py | 7 ++----- homeassistant/components/google_mail/const.py | 8 +++++++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google_mail/__init__.py b/homeassistant/components/google_mail/__init__.py index f1d46178ccc8b..74d8e222e09fc 100644 --- a/homeassistant/components/google_mail/__init__.py +++ b/homeassistant/components/google_mail/__init__.py @@ -1,5 +1,4 @@ """Support for Google Mail.""" -# pylint: disable=home-assistant-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, Platform @@ -26,7 +25,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Google Mail integration.""" - hass.data.setdefault(DOMAIN, {})[DATA_HASS_CONFIG] = config + hass.data[DATA_HASS_CONFIG] = config async_setup_services(hass) @@ -53,9 +52,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoogleMailConfigEntry) - Platform.NOTIFY, DOMAIN, {DATA_AUTH: auth, CONF_NAME: entry.title}, - # Uses legacy hass.data[DOMAIN] pattern - # pylint: disable-next=home-assistant-use-runtime-data - hass.data[DOMAIN][DATA_HASS_CONFIG], + hass.data[DATA_HASS_CONFIG], ) ) diff --git a/homeassistant/components/google_mail/const.py b/homeassistant/components/google_mail/const.py index c03a3f718275e..82c846429bfbf 100644 --- a/homeassistant/components/google_mail/const.py +++ b/homeassistant/components/google_mail/const.py @@ -1,5 +1,9 @@ """Constants for Google Mail integration.""" +from typing import Any + +from homeassistant.util.hass_dict import HassKey + ATTR_BCC = "bcc" ATTR_CC = "cc" ATTR_ENABLED = "enabled" @@ -16,7 +20,9 @@ ATTR_TITLE = "title" DATA_AUTH = "auth" -DATA_HASS_CONFIG = "hass_config" +# Domain-level root HA config for legacy notify platform discovery. +# Not per config entry — entries store auth on entry.runtime_data. +DATA_HASS_CONFIG: HassKey[dict[str, Any]] = HassKey("google_mail_hass_config") DEFAULT_ACCESS = [ "https://www.googleapis.com/auth/gmail.compose", "https://www.googleapis.com/auth/gmail.settings.basic", From bb0ebcb1eb000bdd1cc18669a2733017e98fa5e8 Mon Sep 17 00:00:00 2001 From: ElectricSteve <96793824+electricsteve@users.noreply.github.com> Date: Tue, 4 Aug 2026 00:53:13 +0200 Subject: [PATCH 4/5] Bump jellyfin-apiclient-python to 1.16.0 (#177466) --- homeassistant/components/jellyfin/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/jellyfin/manifest.json b/homeassistant/components/jellyfin/manifest.json index 839d9e685fcd8..785fbe9e4ebc1 100644 --- a/homeassistant/components/jellyfin/manifest.json +++ b/homeassistant/components/jellyfin/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "local_polling", "loggers": ["jellyfin_apiclient_python"], - "requirements": ["jellyfin-apiclient-python==1.11.0"] + "requirements": ["jellyfin-apiclient-python==1.16.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 88ce861813423..08acfb05ff548 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1407,7 +1407,7 @@ israel-rail-api==0.1.5 jaraco.abode==6.4.0 # homeassistant.components.jellyfin -jellyfin-apiclient-python==1.11.0 +jellyfin-apiclient-python==1.16.0 # homeassistant.components.command_line # homeassistant.components.rest From bedc799070c3d73d5a1db5691a956fbb0b0b3357 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 4 Aug 2026 12:23:51 +1000 Subject: [PATCH 5/5] Use library model names in tesla_fleet (#178111) --- .../components/tesla_fleet/__init__.py | 4 ++-- homeassistant/components/tesla_fleet/const.py | 9 -------- tests/components/tesla_fleet/test_init.py | 22 ++++++++++++++++++- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/tesla_fleet/__init__.py b/homeassistant/components/tesla_fleet/__init__.py index 389a6e798929d..d450d1fd16697 100644 --- a/homeassistant/components/tesla_fleet/__init__.py +++ b/homeassistant/components/tesla_fleet/__init__.py @@ -33,7 +33,7 @@ ) from homeassistant.helpers.device_registry import DeviceInfo -from .const import DOMAIN, LOGGER, MODELS +from .const import DOMAIN, LOGGER from .coordinator import ( TeslaFleetEnergySiteHistoryCoordinator, TeslaFleetEnergySiteInfoCoordinator, @@ -183,7 +183,7 @@ async def _get_access_token() -> str: identifiers={(DOMAIN, vin)}, manufacturer="Tesla", name=product["display_name"], - model=MODELS.get(vin[3]), + model=api_vehicle.model, serial_number=vin, ) diff --git a/homeassistant/components/tesla_fleet/const.py b/homeassistant/components/tesla_fleet/const.py index aa83a77f5c614..ed524b6348569 100644 --- a/homeassistant/components/tesla_fleet/const.py +++ b/homeassistant/components/tesla_fleet/const.py @@ -25,15 +25,6 @@ Scope.ENERGY_CMDS, ] -MODELS = { - "S": "Model S", - "3": "Model 3", - "X": "Model X", - "Y": "Model Y", - "C": "Cybertruck", - "T": "Tesla Semi", -} - ENERGY_HISTORY_FIELDS = [ "solar_energy_exported", "generator_energy_exported", diff --git a/tests/components/tesla_fleet/test_init.py b/tests/components/tesla_fleet/test_init.py index 701cf16ed77c4..431fbba38f269 100644 --- a/tests/components/tesla_fleet/test_init.py +++ b/tests/components/tesla_fleet/test_init.py @@ -2,7 +2,7 @@ from copy import deepcopy from datetime import timedelta -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import AsyncMock, Mock, PropertyMock, patch from freezegun.api import FrozenDateTimeFactory import pytest @@ -233,6 +233,26 @@ async def test_devices( assert device == snapshot(name=f"{device.identifiers}") +async def test_vehicle_device_model_from_library( + hass: HomeAssistant, + normal_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test the vehicle device model is provided by the library.""" + with patch( + "tesla_fleet_api.tesla.VehicleFleet.model", + new_callable=PropertyMock, + return_value="Cybercab", + ): + await setup_platform(hass, normal_config_entry) + + device = device_registry.async_get_device( + identifiers={(DOMAIN, "LRWXF7EK4KC700000")} + ) + assert device is not None + assert device.model == "Cybercab" + + # Vehicle Coordinator async def test_vehicle_refresh_offline( hass: HomeAssistant,