diff --git a/homeassistant/components/amazon_devices/manifest.json b/homeassistant/components/amazon_devices/manifest.json index bd9bc701d3e289..37a56486a08429 100644 --- a/homeassistant/components/amazon_devices/manifest.json +++ b/homeassistant/components/amazon_devices/manifest.json @@ -118,5 +118,5 @@ "iot_class": "cloud_polling", "loggers": ["aioamazondevices"], "quality_scale": "bronze", - "requirements": ["aioamazondevices==3.0.5"] + "requirements": ["aioamazondevices==3.0.6"] } diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index da0be245fcd3ba..a6a6e447426d66 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/environment_canada", "iot_class": "cloud_polling", "loggers": ["env_canada"], - "requirements": ["env-canada==0.10.2"] + "requirements": ["env-canada==0.11.2"] } diff --git a/homeassistant/components/hydrawise/manifest.json b/homeassistant/components/hydrawise/manifest.json index 0c355c34a71d0e..03b9dc68a79bab 100644 --- a/homeassistant/components/hydrawise/manifest.json +++ b/homeassistant/components/hydrawise/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/hydrawise", "iot_class": "cloud_polling", "loggers": ["pydrawise"], - "requirements": ["pydrawise==2025.3.0"] + "requirements": ["pydrawise==2025.6.0"] } diff --git a/homeassistant/components/imeon_inverter/entity.py b/homeassistant/components/imeon_inverter/entity.py new file mode 100644 index 00000000000000..e6bd868960697a --- /dev/null +++ b/homeassistant/components/imeon_inverter/entity.py @@ -0,0 +1,40 @@ +"""Imeon inverter base class for entities.""" + +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import InverterCoordinator + +type InverterConfigEntry = ConfigEntry[InverterCoordinator] + + +class InverterEntity(CoordinatorEntity[InverterCoordinator]): + """Common elements for all entities.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: InverterCoordinator, + entry: InverterConfigEntry, + entity_description: EntityDescription, + ) -> None: + """Pass coordinator to CoordinatorEntity.""" + super().__init__(coordinator) + self.entity_description = entity_description + self._inverter = coordinator.api.inverter + self.data_key = entity_description.key + assert entry.unique_id + self._attr_unique_id = f"{entry.unique_id}_{self.data_key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, entry.unique_id)}, + name="Imeon inverter", + manufacturer="Imeon Energy", + model=self._inverter.get("inverter"), + sw_version=self._inverter.get("software"), + serial_number=self._inverter.get("serial"), + configuration_url=self._inverter.get("url"), + ) diff --git a/homeassistant/components/imeon_inverter/sensor.py b/homeassistant/components/imeon_inverter/sensor.py index a2f6ded5ab3e3b..e1d05d0ecf6e5f 100644 --- a/homeassistant/components/imeon_inverter/sensor.py +++ b/homeassistant/components/imeon_inverter/sensor.py @@ -21,20 +21,18 @@ UnitOfTime, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN from .coordinator import InverterCoordinator +from .entity import InverterEntity type InverterConfigEntry = ConfigEntry[InverterCoordinator] _LOGGER = logging.getLogger(__name__) -ENTITY_DESCRIPTIONS = ( +SENSOR_DESCRIPTIONS = ( # Battery SensorEntityDescription( key="battery_autonomy", @@ -423,42 +421,18 @@ async def async_setup_entry( """Create each sensor for a given config entry.""" coordinator = entry.runtime_data - - # Init sensor entities async_add_entities( InverterSensor(coordinator, entry, description) - for description in ENTITY_DESCRIPTIONS + for description in SENSOR_DESCRIPTIONS ) -class InverterSensor(CoordinatorEntity[InverterCoordinator], SensorEntity): - """A sensor that returns numerical values with units.""" +class InverterSensor(InverterEntity, SensorEntity): + """Representation of an Imeon inverter sensor.""" - _attr_has_entity_name = True _attr_entity_category = EntityCategory.DIAGNOSTIC - def __init__( - self, - coordinator: InverterCoordinator, - entry: InverterConfigEntry, - description: SensorEntityDescription, - ) -> None: - """Pass coordinator to CoordinatorEntity.""" - super().__init__(coordinator) - self.entity_description = description - self._inverter = coordinator.api.inverter - self.data_key = description.key - assert entry.unique_id - self._attr_unique_id = f"{entry.unique_id}_{self.data_key}" - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, entry.unique_id)}, - name="Imeon inverter", - manufacturer="Imeon Energy", - model=self._inverter.get("inverter"), - sw_version=self._inverter.get("software"), - ) - @property def native_value(self) -> StateType | None: - """Value of the sensor.""" + """Return the state of the entity.""" return self.coordinator.data.get(self.data_key) diff --git a/homeassistant/components/jellyfin/media_source.py b/homeassistant/components/jellyfin/media_source.py index a4d08d8d02421e..7dc0745a51ece3 100644 --- a/homeassistant/components/jellyfin/media_source.py +++ b/homeassistant/components/jellyfin/media_source.py @@ -329,8 +329,8 @@ async def _build_movies(self, library_id: str) -> list[BrowseMediaSource]: movies = await self._get_children(library_id, ITEM_TYPE_MOVIE) movies = sorted( movies, - # Sort by whether a movies has an name first, then by name - # This allows for sorting moveis with, without and with missing names + # Sort by whether a movie has a name first, then by name + # This allows for sorting movies with, without and with missing names key=lambda k: ( ITEM_KEY_NAME not in k, k.get(ITEM_KEY_NAME), @@ -388,7 +388,7 @@ async def _build_tvshow(self, library_id: str) -> list[BrowseMediaSource]: series = await self._get_children(library_id, ITEM_TYPE_SERIES) series = sorted( series, - # Sort by whether a seroes has an name first, then by name + # Sort by whether a series has a name first, then by name # This allows for sorting series with, without and with missing names key=lambda k: ( ITEM_KEY_NAME not in k, diff --git a/homeassistant/components/nordpool/manifest.json b/homeassistant/components/nordpool/manifest.json index b096d2bd5068a8..ca299b470ea311 100644 --- a/homeassistant/components/nordpool/manifest.json +++ b/homeassistant/components/nordpool/manifest.json @@ -8,6 +8,6 @@ "iot_class": "cloud_polling", "loggers": ["pynordpool"], "quality_scale": "platinum", - "requirements": ["pynordpool==0.2.4"], + "requirements": ["pynordpool==0.3.0"], "single_config_entry": true } diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index 789e9647f77c4a..87b7860afb5e31 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -10,7 +10,6 @@ ) from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - CONCENTRATION_PARTS_PER_MILLION, DEGREE, PERCENTAGE, UV_INDEX, @@ -170,7 +169,7 @@ ), SensorEntityDescription( key=ATTR_API_AIRPOLLUTION_CO, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, device_class=SensorDeviceClass.CO, state_class=SensorStateClass.MEASUREMENT, ), diff --git a/homeassistant/components/telegram_bot/bot.py b/homeassistant/components/telegram_bot/bot.py index f983d0551f7179..1c8684f9a4e40f 100644 --- a/homeassistant/components/telegram_bot/bot.py +++ b/homeassistant/components/telegram_bot/bot.py @@ -241,6 +241,7 @@ def __init__( self._parse_mode = self._parsers.get(parser) self.bot = bot self.hass = hass + self._last_message_id: dict[int, int] = {} def _get_allowed_chat_ids(self) -> list[int]: allowed_chat_ids: list[int] = [ @@ -260,9 +261,6 @@ def _get_allowed_chat_ids(self) -> list[int]: return allowed_chat_ids - def _get_last_message_id(self): - return dict.fromkeys(self._get_allowed_chat_ids()) - def _get_msg_ids(self, msg_data, chat_id): """Get the message id to edit. @@ -277,9 +275,9 @@ def _get_msg_ids(self, msg_data, chat_id): if ( isinstance(message_id, str) and (message_id == "last") - and (self._get_last_message_id()[chat_id] is not None) + and (chat_id in self._last_message_id) ): - message_id = self._get_last_message_id()[chat_id] + message_id = self._last_message_id[chat_id] else: inline_message_id = msg_data["inline_message_id"] return message_id, inline_message_id @@ -408,10 +406,10 @@ async def _send_msg( if not isinstance(out, bool) and hasattr(out, ATTR_MESSAGEID): chat_id = out.chat_id message_id = out[ATTR_MESSAGEID] - self._get_last_message_id()[chat_id] = message_id + self._last_message_id[chat_id] = message_id _LOGGER.debug( "Last message ID: %s (from chat_id %s)", - self._get_last_message_id(), + self._last_message_id, chat_id, ) @@ -480,9 +478,9 @@ async def delete_message(self, chat_id=None, context=None, **kwargs): context=context, ) # reduce message_id anyway: - if self._get_last_message_id()[chat_id] is not None: + if chat_id in self._last_message_id: # change last msg_id for deque(n_msgs)? - self._get_last_message_id()[chat_id] -= 1 + self._last_message_id[chat_id] -= 1 return deleted async def edit_message(self, type_edit, chat_id=None, context=None, **kwargs): diff --git a/requirements_all.txt b/requirements_all.txt index 4813de03016a9f..ce77ca508dae13 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -182,7 +182,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.amazon_devices -aioamazondevices==3.0.5 +aioamazondevices==3.0.6 # homeassistant.components.ambient_network # homeassistant.components.ambient_station @@ -878,7 +878,7 @@ enocean==0.50 enturclient==0.2.4 # homeassistant.components.environment_canada -env-canada==0.10.2 +env-canada==0.11.2 # homeassistant.components.season ephem==4.1.6 @@ -1925,7 +1925,7 @@ pydiscovergy==3.0.2 pydoods==1.0.2 # homeassistant.components.hydrawise -pydrawise==2025.3.0 +pydrawise==2025.6.0 # homeassistant.components.android_ip_webcam pydroid-ipcam==3.0.0 @@ -2174,7 +2174,7 @@ pynina==0.3.6 pynobo==1.8.1 # homeassistant.components.nordpool -pynordpool==0.2.4 +pynordpool==0.3.0 # homeassistant.components.nuki pynuki==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1f57d16a9e8a0e..a5ae1ef0b379ce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -170,7 +170,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.amazon_devices -aioamazondevices==3.0.5 +aioamazondevices==3.0.6 # homeassistant.components.ambient_network # homeassistant.components.ambient_station @@ -757,7 +757,7 @@ energyzero==2.1.1 enocean==0.50 # homeassistant.components.environment_canada -env-canada==0.10.2 +env-canada==0.11.2 # homeassistant.components.season ephem==4.1.6 @@ -1603,7 +1603,7 @@ pydexcom==0.2.3 pydiscovergy==3.0.2 # homeassistant.components.hydrawise -pydrawise==2025.3.0 +pydrawise==2025.6.0 # homeassistant.components.android_ip_webcam pydroid-ipcam==3.0.0 @@ -1804,7 +1804,7 @@ pynina==0.3.6 pynobo==1.8.1 # homeassistant.components.nordpool -pynordpool==0.2.4 +pynordpool==0.3.0 # homeassistant.components.nuki pynuki==1.6.3 diff --git a/tests/components/imeon_inverter/conftest.py b/tests/components/imeon_inverter/conftest.py index 5d1dacc4e69cfd..e147a6ff642a2a 100644 --- a/tests/components/imeon_inverter/conftest.py +++ b/tests/components/imeon_inverter/conftest.py @@ -60,7 +60,12 @@ def mock_imeon_inverter() -> Generator[MagicMock]: inverter.__aenter__.return_value = inverter inverter.login.return_value = True inverter.get_serial.return_value = TEST_SERIAL - inverter.inverter.get.return_value = {"inverter": "blah", "software": "1.0"} + inverter.inverter = { + "inverter": "3.6", + "software": "1.0", + "serial": TEST_SERIAL, + "url": f"http://{TEST_USER_INPUT[CONF_HOST]}", + } inverter.storage = load_json_object_fixture("sensor_data.json", DOMAIN) yield inverter diff --git a/tests/components/imeon_inverter/test_sensor.py b/tests/components/imeon_inverter/test_sensor.py index 19e912c1c5c51e..194864a67a2e97 100644 --- a/tests/components/imeon_inverter/test_sensor.py +++ b/tests/components/imeon_inverter/test_sensor.py @@ -1,6 +1,6 @@ """Test the Imeon Inverter sensors.""" -from unittest.mock import MagicMock, patch +from unittest.mock import patch from syrupy.assertion import SnapshotAssertion @@ -15,15 +15,12 @@ async def test_sensors( hass: HomeAssistant, - mock_imeon_inverter: MagicMock, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, mock_config_entry: MockConfigEntry, ) -> None: """Test the Imeon Inverter sensors.""" - with patch( - "homeassistant.components.imeon_inverter.const.PLATFORMS", [Platform.SENSOR] - ): + with patch("homeassistant.components.imeon_inverter.PLATFORMS", [Platform.SENSOR]): await setup_integration(hass, mock_config_entry) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/openweathermap/snapshots/test_sensor.ambr b/tests/components/openweathermap/snapshots/test_sensor.ambr index cbd86f14676e2b..11a1feb721f70c 100644 --- a/tests/components/openweathermap/snapshots/test_sensor.ambr +++ b/tests/components/openweathermap/snapshots/test_sensor.ambr @@ -86,7 +86,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-co', - 'unit_of_measurement': 'ppm', + 'unit_of_measurement': 'µg/m³', }) # --- # name: test_sensor_states[air_pollution][sensor.openweathermap_carbon_monoxide-state] @@ -96,7 +96,7 @@ 'device_class': 'carbon_monoxide', 'friendly_name': 'openweathermap Carbon monoxide', 'state_class': , - 'unit_of_measurement': 'ppm', + 'unit_of_measurement': 'µg/m³', }), 'context': , 'entity_id': 'sensor.openweathermap_carbon_monoxide', diff --git a/tests/components/telegram_bot/conftest.py b/tests/components/telegram_bot/conftest.py index 2b364af497eef5..6886a8af6e5ab8 100644 --- a/tests/components/telegram_bot/conftest.py +++ b/tests/components/telegram_bot/conftest.py @@ -255,8 +255,8 @@ def mock_broadcast_config_entry() -> MockConfigEntry: options={ATTR_PARSER: PARSER_MD}, subentries_data=[ ConfigSubentryData( - unique_id="1234567890", - data={CONF_CHAT_ID: 1234567890}, + unique_id="123456", + data={CONF_CHAT_ID: 123456}, subentry_id="mock_id", subentry_type=CONF_ALLOWED_CHAT_IDS, title="mock chat", diff --git a/tests/components/telegram_bot/test_config_flow.py b/tests/components/telegram_bot/test_config_flow.py index 09c8d99472a066..62a0c02b979906 100644 --- a/tests/components/telegram_bot/test_config_flow.py +++ b/tests/components/telegram_bot/test_config_flow.py @@ -413,7 +413,7 @@ async def test_subentry_flow_chat_error( with patch( "homeassistant.components.telegram_bot.config_flow.Bot.get_chat", return_value=ChatFullInfo( - id=1234567890, + id=123456, title="mock title", first_name="mock first_name", type="PRIVATE", @@ -423,7 +423,7 @@ async def test_subentry_flow_chat_error( ): result = await hass.config_entries.subentries.async_configure( result["flow_id"], - user_input={CONF_CHAT_ID: 1234567890}, + user_input={CONF_CHAT_ID: 123456}, ) await hass.async_block_till_done() diff --git a/tests/components/telegram_bot/test_telegram_bot.py b/tests/components/telegram_bot/test_telegram_bot.py index 928c957902086f..60e85394f58789 100644 --- a/tests/components/telegram_bot/test_telegram_bot.py +++ b/tests/components/telegram_bot/test_telegram_bot.py @@ -17,8 +17,10 @@ from homeassistant.components.telegram_bot import ( ATTR_CALLBACK_QUERY_ID, + ATTR_CAPTION, ATTR_CHAT_ID, ATTR_FILE, + ATTR_KEYBOARD_INLINE, ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_MESSAGE, @@ -34,7 +36,9 @@ PLATFORM_BROADCAST, SERVICE_ANSWER_CALLBACK_QUERY, SERVICE_DELETE_MESSAGE, + SERVICE_EDIT_CAPTION, SERVICE_EDIT_MESSAGE, + SERVICE_EDIT_REPLYMARKUP, SERVICE_SEND_ANIMATION, SERVICE_SEND_DOCUMENT, SERVICE_SEND_LOCATION, @@ -629,14 +633,23 @@ async def test_delete_message( await hass.config_entries.async_setup(mock_broadcast_config_entry.entry_id) await hass.async_block_till_done() + response = await hass.services.async_call( + DOMAIN, + SERVICE_SEND_MESSAGE, + {ATTR_MESSAGE: "mock message"}, + blocking=True, + return_response=True, + ) + assert response["chats"][0]["message_id"] == 12345 + with patch( - "homeassistant.components.telegram_bot.bot.TelegramNotificationService.delete_message", + "homeassistant.components.telegram_bot.bot.Bot.delete_message", AsyncMock(return_value=True), ) as mock: await hass.services.async_call( DOMAIN, SERVICE_DELETE_MESSAGE, - {ATTR_CHAT_ID: 12345, ATTR_MESSAGEID: 12345}, + {ATTR_CHAT_ID: 123456, ATTR_MESSAGEID: "last"}, blocking=True, ) @@ -655,7 +668,7 @@ async def test_edit_message( await hass.async_block_till_done() with patch( - "homeassistant.components.telegram_bot.bot.TelegramNotificationService.edit_message", + "homeassistant.components.telegram_bot.bot.Bot.edit_message_text", AsyncMock(return_value=True), ) as mock: await hass.services.async_call( @@ -668,6 +681,34 @@ async def test_edit_message( await hass.async_block_till_done() mock.assert_called_once() + with patch( + "homeassistant.components.telegram_bot.bot.Bot.edit_message_caption", + AsyncMock(return_value=True), + ) as mock: + await hass.services.async_call( + DOMAIN, + SERVICE_EDIT_CAPTION, + {ATTR_CAPTION: "mock caption", ATTR_CHAT_ID: 12345, ATTR_MESSAGEID: 12345}, + blocking=True, + ) + + await hass.async_block_till_done() + mock.assert_called_once() + + with patch( + "homeassistant.components.telegram_bot.bot.Bot.edit_message_reply_markup", + AsyncMock(return_value=True), + ) as mock: + await hass.services.async_call( + DOMAIN, + SERVICE_EDIT_REPLYMARKUP, + {ATTR_KEYBOARD_INLINE: [], ATTR_CHAT_ID: 12345, ATTR_MESSAGEID: 12345}, + blocking=True, + ) + + await hass.async_block_till_done() + mock.assert_called_once() + async def test_async_setup_entry_failed( hass: HomeAssistant, mock_broadcast_config_entry: MockConfigEntry