diff --git a/homeassistant/components/google_health/quality_scale.yaml b/homeassistant/components/google_health/quality_scale.yaml index eab8c89d611dcb..a00be31878a7f4 100644 --- a/homeassistant/components/google_health/quality_scale.yaml +++ b/homeassistant/components/google_health/quality_scale.yaml @@ -64,9 +64,7 @@ rules: docs-troubleshooting: done docs-use-cases: done dynamic-devices: done - entity-category: - status: exempt - comment: All entities are user-facing primary sensors. + entity-category: done entity-device-class: done entity-disabled-by-default: status: exempt diff --git a/homeassistant/components/google_health/sensor.py b/homeassistant/components/google_health/sensor.py index 53a1a3722dc337..59df5b2f89e9c2 100644 --- a/homeassistant/components/google_health/sensor.py +++ b/homeassistant/components/google_health/sensor.py @@ -15,6 +15,7 @@ ) from homeassistant.const import ( PERCENTAGE, + EntityCategory, UnitOfEnergy, UnitOfLength, UnitOfMass, @@ -81,6 +82,7 @@ class GoogleHealthSensorEntityDescription[ key="active_calories", translation_key="active_calories", native_unit_of_measurement=UnitOfEnergy.KILO_CALORIE, + device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, value_fn=lambda data: ( data.active_energy_burned.kcal_sum @@ -92,6 +94,7 @@ class GoogleHealthSensorEntityDescription[ key="total_calories", translation_key="total_calories", native_unit_of_measurement=UnitOfEnergy.KILO_CALORIE, + device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, value_fn=lambda data: ( data.total_calories.kcal_sum if data and data.total_calories else 0.0 @@ -230,6 +233,7 @@ class GoogleHealthSensorEntityDescription[ key="calories_consumed", translation_key="calories_consumed", native_unit_of_measurement=UnitOfEnergy.KILO_CALORIE, + device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, value_fn=lambda data: ( data.nutrition.energy.kcal_sum @@ -374,6 +378,7 @@ class GoogleHealthDeviceSensor( """Device-specific Google Health sensor entity.""" _attr_has_entity_name = True + _attr_entity_category = EntityCategory.DIAGNOSTIC entity_description: GoogleHealthDeviceSensorEntityDescription def __init__( diff --git a/homeassistant/components/myuplink/coordinator.py b/homeassistant/components/myuplink/coordinator.py index f1bab8995a1168..fae248e350144a 100644 --- a/homeassistant/components/myuplink/coordinator.py +++ b/homeassistant/components/myuplink/coordinator.py @@ -2,7 +2,7 @@ import asyncio.timeouts from dataclasses import dataclass -from datetime import datetime, timedelta +from datetime import timedelta import logging from typing import override @@ -22,7 +22,6 @@ class CoordinatorData: systems: list[System] devices: dict[str, Device] points: dict[str, dict[str, DevicePoint]] - time: datetime type MyUplinkConfigEntry = ConfigEntry[MyUplinkDataCoordinator] @@ -75,5 +74,4 @@ async def _async_update_data(self) -> CoordinatorData: systems=systems, devices=devices, points=points, - time=datetime.now(), # pylint: disable=home-assistant-enforce-naive-now ) diff --git a/homeassistant/components/tado/coordinator.py b/homeassistant/components/tado/coordinator.py index cc55876194e229..9be21aab8867a6 100644 --- a/homeassistant/components/tado/coordinator.py +++ b/homeassistant/components/tado/coordinator.py @@ -448,7 +448,7 @@ async def set_temperature_offset(self, device_id, offset): async def set_meter_reading(self, reading: int) -> dict[str, Any]: """Send meter reading to Tado.""" - dt: str = datetime.now().strftime("%Y-%m-%d") # pylint: disable=home-assistant-enforce-naive-now + dt: str = dt_util.now().strftime("%Y-%m-%d") if self._tado is None: raise HomeAssistantError("Tado client is not initialized") diff --git a/homeassistant/components/teslemetry/coordinator.py b/homeassistant/components/teslemetry/coordinator.py index fa80e55ddb6fb7..71722939481634 100644 --- a/homeassistant/components/teslemetry/coordinator.py +++ b/homeassistant/components/teslemetry/coordinator.py @@ -1,6 +1,6 @@ """Teslemetry Data Coordinator.""" -from datetime import datetime, timedelta +from datetime import timedelta from typing import TYPE_CHECKING, Any, override from tesla_fleet_api.const import TeslaEnergyPeriod, VehicleDataEndpoint @@ -113,7 +113,6 @@ class TeslemetryVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Class to manage fetching data from the Teslemetry API.""" config_entry: TeslemetryConfigEntry - last_active: datetime def __init__( self, @@ -135,7 +134,6 @@ def __init__( self.api = api self.data = flatten(product) - self.last_active = datetime.now() # pylint: disable=home-assistant-enforce-naive-now @override async def _async_update_data(self) -> dict[str, Any]: diff --git a/homeassistant/components/watts/const.py b/homeassistant/components/watts/const.py index e1ba4a0134e09b..8002762467fab6 100644 --- a/homeassistant/components/watts/const.py +++ b/homeassistant/components/watts/const.py @@ -23,7 +23,7 @@ # Update intervals UPDATE_INTERVAL_SECONDS = 30 FAST_POLLING_INTERVAL_SECONDS = 5 -DISCOVERY_INTERVAL_MINUTES = 15 +DISCOVERY_INTERVAL_SECONDS = 15 * 60 # Mapping from Watts Vision+ modes to Home Assistant HVAC modes THERMOSTAT_MODE_TO_HVAC: dict[ThermostatMode, HVACMode] = { diff --git a/homeassistant/components/watts/coordinator.py b/homeassistant/components/watts/coordinator.py index 754e268d8c90a6..28982b63ec0517 100644 --- a/homeassistant/components/watts/coordinator.py +++ b/homeassistant/components/watts/coordinator.py @@ -1,8 +1,9 @@ """Data coordinator for Watts Vision integration.""" from dataclasses import dataclass -from datetime import datetime, timedelta +from datetime import timedelta import logging +import time from typing import TYPE_CHECKING, override from visionpluspython.client import WattsVisionClient @@ -22,7 +23,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( - DISCOVERY_INTERVAL_MINUTES, + DISCOVERY_INTERVAL_SECONDS, DOMAIN, FAST_POLLING_INTERVAL_SECONDS, UPDATE_INTERVAL_SECONDS, @@ -61,18 +62,17 @@ def __init__( config_entry=config_entry, ) self.client = client - self.last_discovery: datetime | None = None + self.last_discovery: float | None = None self.previous_devices: set[str] = set() @override async def _async_update_data(self) -> dict[str, Device]: """Fetch data and periodic device discovery.""" - now = datetime.now() # pylint: disable=home-assistant-enforce-naive-now + now = time.time() is_first_refresh = self.last_discovery is None discovery_interval_elapsed = ( self.last_discovery is not None - and now - self.last_discovery - >= timedelta(minutes=DISCOVERY_INTERVAL_MINUTES) + and now - self.last_discovery >= DISCOVERY_INTERVAL_SECONDS ) if is_first_refresh or discovery_interval_elapsed: @@ -185,7 +185,7 @@ def __init__( self.client = client self.device_id = device_id self.hub_coordinator = hub_coordinator - self.fast_polling_until: datetime | None = None + self.fast_polling_until: float | None = None # Listen to hub coordinator updates self.unsubscribe_hub_listener = hub_coordinator.async_add_listener( @@ -208,7 +208,7 @@ def _handle_hub_update(self) -> None: @override async def _async_update_data(self) -> WattsVisionDeviceData: """Refresh specific device.""" - if self.fast_polling_until and datetime.now() > self.fast_polling_until: # pylint: disable=home-assistant-enforce-naive-now + if self.fast_polling_until and time.time() > self.fast_polling_until: self.fast_polling_until = None self.update_interval = None _LOGGER.debug( @@ -244,10 +244,12 @@ async def _async_update_data(self) -> WattsVisionDeviceData: _LOGGER.debug("Refreshed device %s", self.device_id) return WattsVisionDeviceData(device=device) - def trigger_fast_polling(self, duration: int = 60) -> None: + def trigger_fast_polling(self, duration_seconds: int = 60) -> None: """Activate fast polling for a specified duration after a command.""" - self.fast_polling_until = datetime.now() + timedelta(seconds=duration) # pylint: disable=home-assistant-enforce-naive-now + self.fast_polling_until = time.time() + duration_seconds self.update_interval = timedelta(seconds=FAST_POLLING_INTERVAL_SECONDS) _LOGGER.debug( - "Device %s: Activated fast polling for %d seconds", self.device_id, duration + "Device %s: Activated fast polling for %d seconds", + self.device_id, + duration_seconds, ) diff --git a/homeassistant/components/watts/diagnostics.py b/homeassistant/components/watts/diagnostics.py index ece46d9cf79a71..53b94c54bf97f4 100644 --- a/homeassistant/components/watts/diagnostics.py +++ b/homeassistant/components/watts/diagnostics.py @@ -1,12 +1,13 @@ """Diagnostics support for Watts Vision +.""" import dataclasses -from datetime import datetime +import time from typing import Any from homeassistant.components.diagnostics import async_redact_data from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util from . import WattsVisionConfigEntry @@ -21,7 +22,7 @@ async def async_get_config_entry_diagnostics( runtime_data = entry.runtime_data hub_coordinator = runtime_data.hub_coordinator device_coordinators = runtime_data.device_coordinators - now = datetime.now() # pylint: disable=home-assistant-enforce-naive-now + now = time.time() return async_redact_data( { @@ -34,7 +35,9 @@ async def async_get_config_entry_diagnostics( else None ), "last_discovery": ( - hub_coordinator.last_discovery.isoformat() + dt_util.utc_from_timestamp( + hub_coordinator.last_discovery + ).isoformat() if hub_coordinator.last_discovery else None ), @@ -54,7 +57,9 @@ async def async_get_config_entry_diagnostics( and coordinator.fast_polling_until > now ), "fast_polling_until": ( - coordinator.fast_polling_until.isoformat() + dt_util.utc_from_timestamp( + coordinator.fast_polling_until + ).isoformat() if coordinator.fast_polling_until is not None and coordinator.fast_polling_until > now else None diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index c19060a9435ed9..340caab8481c67 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -696,11 +696,15 @@ async def async_register_node_in_dev_reg(self, node: ZwaveNode) -> dr.DeviceEntr node_id_device = self.dev_reg.async_get_device_by_identifier( device_id, self.config_entry.entry_id ) - via_identifier = None + via_device_id: str | None = None controller = driver.controller # Get the controller node device ID if this node is not the controller if controller.own_node and controller.own_node != node: - via_identifier = get_device_id(driver, controller.own_node) + via_device_id = dr.async_get_device_id_by_identifier( + self.hass, + get_device_id(driver, controller.own_node), + config_entry_id=self.config_entry.entry_id, + ) if device_id_ext: # If there is a device with this node ID but with a different hardware @@ -747,7 +751,7 @@ async def async_register_node_in_dev_reg(self, node: ZwaveNode) -> dr.DeviceEntr model=node.device_config.label, manufacturer=node.device_config.manufacturer, suggested_area=node.location or UNDEFINED, - via_device=via_identifier, + via_device_id=via_device_id, ) async_dispatcher_send(self.hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index d8af704cf05c0f..abeeed3ffce319 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -1141,6 +1141,14 @@ async def websocket_provision_smart_start_node( manufacturer = device_info.manufacturer model = device_info.label + via_device_id: str | None = None + if driver.controller.own_node: + via_device_id = dr.async_get_device_id_by_identifier( + hass, + get_device_id(driver, driver.controller.own_node), + config_entry_id=entry.entry_id, + ) + # Create an empty device device = dev_reg.async_get_or_create( config_entry_id=entry.entry_id, @@ -1148,11 +1156,7 @@ async def websocket_provision_smart_start_node( name=device_name, manufacturer=manufacturer, model=model, - via_device=( - get_device_id(driver, driver.controller.own_node) - if driver.controller.own_node - else None - ), + via_device_id=via_device_id, ) dev_reg.async_update_device( device.id, area_id=msg.get(AREA_ID), name_by_user=device_name diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index f4549792bb9b53..9451757998c63e 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -623,8 +623,6 @@ def report_not_triggered(reason: str, /, **data: Any) -> None: if not self.is_valid_state(to_state, report_not_triggered): return - # The trigger should never fire if the origin state is excluded - # or the transition is not valid. if ( from_state.state in self._excluded_from_states or not self.is_valid_transition(from_state, to_state) @@ -657,9 +655,6 @@ def report_not_triggered(reason: str, /, **data: Any) -> None: @callback def call_action() -> None: """Call action with right context.""" - # After a `for` delay, keep the original triggering event payload. - # `async_track_same_state` only verifies the state remained valid - # for the configured duration before firing the action. run_action( { ATTR_ENTITY_ID: entity_id, @@ -672,7 +667,6 @@ def call_action() -> None: ) if not self._duration: - # Call action immediately if duration is not specified or 0 call_action() return diff --git a/tests/components/google_health/snapshots/test_sensor.ambr b/tests/components/google_health/snapshots/test_sensor.ambr index a1a0471943a283..5f084d060e537e 100644 --- a/tests/components/google_health/snapshots/test_sensor.ambr +++ b/tests/components/google_health/snapshots/test_sensor.ambr @@ -14,7 +14,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.device_battery', 'has_entity_name': True, 'hidden_by': None, @@ -67,7 +67,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.device_last_sync_time', 'has_entity_name': True, 'hidden_by': None, @@ -120,7 +120,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.fitbit_charge_6_battery', 'has_entity_name': True, 'hidden_by': None, @@ -173,7 +173,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.fitbit_charge_6_last_sync_time', 'has_entity_name': True, 'hidden_by': None, @@ -237,8 +237,11 @@ 'name': None, 'object_id_base': 'Active calories', 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Active calories', 'platform': 'google_health', @@ -253,6 +256,7 @@ # name: test_all_entities[sensor.google_health_active_calories-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + : 'energy', : 'Google Health Active calories', : , : , @@ -345,8 +349,11 @@ 'name': None, 'object_id_base': 'Calories consumed', 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Calories consumed', 'platform': 'google_health', @@ -361,6 +368,7 @@ # name: test_all_entities[sensor.google_health_calories_consumed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + : 'energy', : 'Google Health Calories consumed', : , : , @@ -911,8 +919,11 @@ 'name': None, 'object_id_base': 'Total calories', 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Total calories', 'platform': 'google_health', @@ -927,6 +938,7 @@ # name: test_all_entities[sensor.google_health_total_calories-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + : 'energy', : 'Google Health Total calories', : , : , @@ -1070,7 +1082,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.scale_battery', 'has_entity_name': True, 'hidden_by': None, @@ -1123,7 +1135,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.scale_last_sync_time', 'has_entity_name': True, 'hidden_by': None, diff --git a/tests/components/midea/test_climate.py b/tests/components/midea/test_climate.py index 14053f2c5e61fa..6aa60b4ab7e9fc 100644 --- a/tests/components/midea/test_climate.py +++ b/tests/components/midea/test_climate.py @@ -2,6 +2,7 @@ from collections.abc import Callable from typing import Any +from unittest.mock import patch from midealocal.const import DeviceType from midealocal.devices.ac import DeviceAttributes as ACAttributes @@ -44,7 +45,7 @@ HVACMode, ) from homeassistant.components.midea.climate import FAN_FULL_SPEED, FAN_SILENT -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er @@ -1460,6 +1461,7 @@ async def test_climate_state_snapshot( ) -> None: """Test async_setup_entry creates entities for each device type.""" config_entry = mock_config_entry(device) - await setup_integration(hass, config_entry, device) + with patch("homeassistant.components.midea._PLATFORMS", [Platform.CLIMATE]): + await setup_integration(hass, config_entry, device) - await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) diff --git a/tests/components/ntfy/conftest.py b/tests/components/ntfy/conftest.py index 44b55bcb70f35d..892c4150090ea7 100644 --- a/tests/components/ntfy/conftest.py +++ b/tests/components/ntfy/conftest.py @@ -45,8 +45,7 @@ def mock_aiontfy() -> Generator[AsyncMock]: load_fixture("account.json", DOMAIN) ) client.generate_token.return_value = AccountTokenResponse( - token="token", - last_access=datetime.now(), # pylint: disable=home-assistant-enforce-naive-now + token="token", last_access=datetime(1970, 1, 1, 0, 0, 0, tzinfo=UTC) ) client.version.return_value = Version.from_json( load_fixture("version.json", DOMAIN) diff --git a/tests/components/ntfy/test_config_flow.py b/tests/components/ntfy/test_config_flow.py index 37ed5c9531135b..82bb6b335adfaa 100644 --- a/tests/components/ntfy/test_config_flow.py +++ b/tests/components/ntfy/test_config_flow.py @@ -1,6 +1,6 @@ """Test the ntfy config flow.""" -from datetime import datetime +from datetime import UTC, datetime from typing import Any from unittest.mock import AsyncMock @@ -450,8 +450,7 @@ async def test_flow_reauth( }, ) mock_aiontfy.generate_token.return_value = AccountTokenResponse( - token="newtoken", - last_access=datetime.now(), # pylint: disable=home-assistant-enforce-naive-now + token="newtoken", last_access=datetime(1970, 1, 1, 0, 0, 0, tzinfo=UTC) ) config_entry.add_to_hass(hass) result = await config_entry.start_reauth_flow(hass) @@ -510,8 +509,7 @@ async def test_form_reauth_errors( ) mock_aiontfy.account.side_effect = exception mock_aiontfy.generate_token.return_value = AccountTokenResponse( - token="newtoken", - last_access=datetime.now(), # pylint: disable=home-assistant-enforce-naive-now + token="newtoken", last_access=datetime(1970, 1, 1, 0, 0, 0, tzinfo=UTC) ) config_entry.add_to_hass(hass) result = await config_entry.start_reauth_flow(hass) @@ -597,8 +595,7 @@ async def test_flow_reconfigure( }, ) mock_aiontfy.generate_token.return_value = AccountTokenResponse( - token="newtoken", - last_access=datetime.now(), # pylint: disable=home-assistant-enforce-naive-now + token="newtoken", last_access=datetime(1970, 1, 1, 0, 0, 0, tzinfo=UTC) ) config_entry.add_to_hass(hass) result = await config_entry.start_reconfigure_flow(hass) @@ -700,8 +697,7 @@ async def test_flow_reconfigure_errors( }, ) mock_aiontfy.generate_token.return_value = AccountTokenResponse( - token="newtoken", - last_access=datetime.now(), # pylint: disable=home-assistant-enforce-naive-now + token="newtoken", last_access=datetime(1970, 1, 1, 0, 0, 0, tzinfo=UTC) ) mock_aiontfy.account.side_effect = exception diff --git a/tests/components/watts/snapshots/test_diagnostics.ambr b/tests/components/watts/snapshots/test_diagnostics.ambr index 072c4181dc59eb..ffc330f17add02 100644 --- a/tests/components/watts/snapshots/test_diagnostics.ambr +++ b/tests/components/watts/snapshots/test_diagnostics.ambr @@ -102,7 +102,7 @@ 'version': 1, }), 'hub_coordinator': dict({ - 'last_discovery': '2026-01-01T12:00:00', + 'last_discovery': '2026-01-01T12:00:00+00:00', 'last_exception': None, 'last_update_success': True, 'supported_devices': 3, diff --git a/tests/components/watts/test_init.py b/tests/components/watts/test_init.py index acca220ce125a4..2044e5cedd23c7 100644 --- a/tests/components/watts/test_init.py +++ b/tests/components/watts/test_init.py @@ -21,7 +21,7 @@ SERVICE_SET_TEMPERATURE, ) from homeassistant.components.watts.const import ( - DISCOVERY_INTERVAL_MINUTES, + DISCOVERY_INTERVAL_SECONDS, DOMAIN, FAST_POLLING_INTERVAL_SECONDS, OAUTH2_TOKEN, @@ -230,7 +230,7 @@ async def test_dynamic_device_creation( current_devices = list(mock_watts_client.discover_devices.return_value) mock_watts_client.discover_devices.return_value = [*current_devices, new_device] - freezer.tick(timedelta(minutes=DISCOVERY_INTERVAL_MINUTES)) + freezer.tick(timedelta(seconds=DISCOVERY_INTERVAL_SECONDS)) async_fire_time_changed(hass) await hass.async_block_till_done() @@ -270,7 +270,7 @@ async def test_stale_device_removal( d for d in current_devices if d.device_id != "thermostat_456" ] - freezer.tick(timedelta(minutes=DISCOVERY_INTERVAL_MINUTES)) + freezer.tick(timedelta(seconds=DISCOVERY_INTERVAL_SECONDS)) async_fire_time_changed(hass) await hass.async_block_till_done()