diff --git a/homeassistant/components/blebox/sensor.py b/homeassistant/components/blebox/sensor.py index 5120a7a3c988f2..14cddf41e45af5 100644 --- a/homeassistant/components/blebox/sensor.py +++ b/homeassistant/components/blebox/sensor.py @@ -1,5 +1,7 @@ """BleBox sensor entities.""" +from datetime import datetime + import blebox_uniapi.sensor from homeassistant.components.sensor import ( @@ -146,7 +148,7 @@ def native_value(self): return self._feature.native_value @property - def last_reset(self): + def last_reset(self) -> datetime | None: """Return the time when the sensor was last reset, if implemented.""" native_implementation = getattr(self._feature, "last_reset", None) diff --git a/homeassistant/components/ebusd/__init__.py b/homeassistant/components/ebusd/__init__.py index 5c36c311bffe95..a32993af0943d9 100644 --- a/homeassistant/components/ebusd/__init__.py +++ b/homeassistant/components/ebusd/__init__.py @@ -1,6 +1,7 @@ """Support for Ebusd daemon for communication with eBUS heating systems.""" import logging +from typing import Any import ebusdpy import voluptuous as vol @@ -17,7 +18,7 @@ from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN, SENSOR_TYPES +from .const import DOMAIN, EBUSD_DATA, SENSOR_TYPES _LOGGER = logging.getLogger(__name__) @@ -28,9 +29,9 @@ SERVICE_EBUSD_WRITE = "ebusd_write" -def verify_ebusd_config(config): +def verify_ebusd_config(config: ConfigType) -> ConfigType: """Verify eBusd config.""" - circuit = config[CONF_CIRCUIT] + circuit: str = config[CONF_CIRCUIT] for condition in config[CONF_MONITORED_CONDITIONS]: if condition not in SENSOR_TYPES[circuit]: raise vol.Invalid(f"Condition '{condition}' not in '{circuit}'.") @@ -59,17 +60,17 @@ def verify_ebusd_config(config): def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the eBusd component.""" _LOGGER.debug("Integration setup started") - conf = config[DOMAIN] - name = conf[CONF_NAME] - circuit = conf[CONF_CIRCUIT] - monitored_conditions = conf.get(CONF_MONITORED_CONDITIONS) - server_address = (conf.get(CONF_HOST), conf.get(CONF_PORT)) + conf: ConfigType = config[DOMAIN] + name: str = conf[CONF_NAME] + circuit: str = conf[CONF_CIRCUIT] + monitored_conditions: list[str] = conf[CONF_MONITORED_CONDITIONS] + server_address: tuple[str, int] = (conf[CONF_HOST], conf[CONF_PORT]) try: ebusdpy.init(server_address) except (TimeoutError, OSError): return False - hass.data[DOMAIN] = EbusdData(server_address, circuit) + hass.data[EBUSD_DATA] = EbusdData(server_address, circuit) sensor_config = { CONF_MONITORED_CONDITIONS: monitored_conditions, "client_name": name, @@ -77,7 +78,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: } load_platform(hass, Platform.SENSOR, DOMAIN, sensor_config, config) - hass.services.register(DOMAIN, SERVICE_EBUSD_WRITE, hass.data[DOMAIN].write) + hass.services.register(DOMAIN, SERVICE_EBUSD_WRITE, hass.data[EBUSD_DATA].write) _LOGGER.debug("Ebusd integration setup completed") return True @@ -86,13 +87,13 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: class EbusdData: """Get the latest data from Ebusd.""" - def __init__(self, address, circuit): + def __init__(self, address: tuple[str, int], circuit: str) -> None: """Initialize the data object.""" self._circuit = circuit self._address = address - self.value = {} + self.value: dict[str, Any] = {} - def update(self, name, stype): + def update(self, name: str, stype: int) -> None: """Call the Ebusd API to update the data.""" try: _LOGGER.debug("Opening socket to ebusd %s", name) diff --git a/homeassistant/components/ebusd/const.py b/homeassistant/components/ebusd/const.py index 4fb3032e19b4d1..10e46f6a2b9b56 100644 --- a/homeassistant/components/ebusd/const.py +++ b/homeassistant/components/ebusd/const.py @@ -1,5 +1,9 @@ """Constants for ebus component.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( PERCENTAGE, @@ -8,277 +12,283 @@ UnitOfTemperature, UnitOfTime, ) +from homeassistant.util.hass_dict import HassKey + +if TYPE_CHECKING: + from . import EbusdData DOMAIN = "ebusd" +EBUSD_DATA: HassKey[EbusdData] = HassKey(DOMAIN) # SensorTypes from ebusdpy module : # 0='decimal', 1='time-schedule', 2='switch', 3='string', 4='value;status' -SENSOR_TYPES = { +type SensorSpecs = tuple[str, str | None, str | None, int, SensorDeviceClass | None] +SENSOR_TYPES: dict[str, dict[str, SensorSpecs]] = { "700": { - "ActualFlowTemperatureDesired": [ + "ActualFlowTemperatureDesired": ( "Hc1ActualFlowTempDesired", UnitOfTemperature.CELSIUS, None, 0, SensorDeviceClass.TEMPERATURE, - ], - "MaxFlowTemperatureDesired": [ + ), + "MaxFlowTemperatureDesired": ( "Hc1MaxFlowTempDesired", UnitOfTemperature.CELSIUS, None, 0, SensorDeviceClass.TEMPERATURE, - ], - "MinFlowTemperatureDesired": [ + ), + "MinFlowTemperatureDesired": ( "Hc1MinFlowTempDesired", UnitOfTemperature.CELSIUS, None, 0, SensorDeviceClass.TEMPERATURE, - ], - "PumpStatus": ["Hc1PumpStatus", None, "mdi:toggle-switch", 2, None], - "HCSummerTemperatureLimit": [ + ), + "PumpStatus": ("Hc1PumpStatus", None, "mdi:toggle-switch", 2, None), + "HCSummerTemperatureLimit": ( "Hc1SummerTempLimit", UnitOfTemperature.CELSIUS, "mdi:weather-sunny", 0, SensorDeviceClass.TEMPERATURE, - ], - "HolidayTemperature": [ + ), + "HolidayTemperature": ( "HolidayTemp", UnitOfTemperature.CELSIUS, None, 0, SensorDeviceClass.TEMPERATURE, - ], - "HWTemperatureDesired": [ + ), + "HWTemperatureDesired": ( "HwcTempDesired", UnitOfTemperature.CELSIUS, None, 0, SensorDeviceClass.TEMPERATURE, - ], - "HWActualTemperature": [ + ), + "HWActualTemperature": ( "HwcStorageTemp", UnitOfTemperature.CELSIUS, None, 0, SensorDeviceClass.TEMPERATURE, - ], - "HWTimerMonday": ["hwcTimer.Monday", None, "mdi:timer-outline", 1, None], - "HWTimerTuesday": ["hwcTimer.Tuesday", None, "mdi:timer-outline", 1, None], - "HWTimerWednesday": ["hwcTimer.Wednesday", None, "mdi:timer-outline", 1, None], - "HWTimerThursday": ["hwcTimer.Thursday", None, "mdi:timer-outline", 1, None], - "HWTimerFriday": ["hwcTimer.Friday", None, "mdi:timer-outline", 1, None], - "HWTimerSaturday": ["hwcTimer.Saturday", None, "mdi:timer-outline", 1, None], - "HWTimerSunday": ["hwcTimer.Sunday", None, "mdi:timer-outline", 1, None], - "HWOperativeMode": ["HwcOpMode", None, "mdi:math-compass", 3, None], - "WaterPressure": [ + ), + "HWTimerMonday": ("hwcTimer.Monday", None, "mdi:timer-outline", 1, None), + "HWTimerTuesday": ("hwcTimer.Tuesday", None, "mdi:timer-outline", 1, None), + "HWTimerWednesday": ("hwcTimer.Wednesday", None, "mdi:timer-outline", 1, None), + "HWTimerThursday": ("hwcTimer.Thursday", None, "mdi:timer-outline", 1, None), + "HWTimerFriday": ("hwcTimer.Friday", None, "mdi:timer-outline", 1, None), + "HWTimerSaturday": ("hwcTimer.Saturday", None, "mdi:timer-outline", 1, None), + "HWTimerSunday": ("hwcTimer.Sunday", None, "mdi:timer-outline", 1, None), + "HWOperativeMode": ("HwcOpMode", None, "mdi:math-compass", 3, None), + "WaterPressure": ( "WaterPressure", UnitOfPressure.BAR, "mdi:water-pump", 0, SensorDeviceClass.PRESSURE, - ], - "Zone1RoomZoneMapping": ["z1RoomZoneMapping", None, "mdi:label", 0, None], - "Zone1NightTemperature": [ + ), + "Zone1RoomZoneMapping": ("z1RoomZoneMapping", None, "mdi:label", 0, None), + "Zone1NightTemperature": ( "z1NightTemp", UnitOfTemperature.CELSIUS, "mdi:weather-night", 0, SensorDeviceClass.TEMPERATURE, - ], - "Zone1DayTemperature": [ + ), + "Zone1DayTemperature": ( "z1DayTemp", UnitOfTemperature.CELSIUS, "mdi:weather-sunny", 0, SensorDeviceClass.TEMPERATURE, - ], - "Zone1HolidayTemperature": [ + ), + "Zone1HolidayTemperature": ( "z1HolidayTemp", UnitOfTemperature.CELSIUS, None, 0, SensorDeviceClass.TEMPERATURE, - ], - "Zone1RoomTemperature": [ + ), + "Zone1RoomTemperature": ( "z1RoomTemp", UnitOfTemperature.CELSIUS, None, 0, SensorDeviceClass.TEMPERATURE, - ], - "Zone1ActualRoomTemperatureDesired": [ + ), + "Zone1ActualRoomTemperatureDesired": ( "z1ActualRoomTempDesired", UnitOfTemperature.CELSIUS, None, 0, SensorDeviceClass.TEMPERATURE, - ], - "Zone1TimerMonday": ["z1Timer.Monday", None, "mdi:timer-outline", 1, None], - "Zone1TimerTuesday": ["z1Timer.Tuesday", None, "mdi:timer-outline", 1, None], - "Zone1TimerWednesday": [ + ), + "Zone1TimerMonday": ("z1Timer.Monday", None, "mdi:timer-outline", 1, None), + "Zone1TimerTuesday": ("z1Timer.Tuesday", None, "mdi:timer-outline", 1, None), + "Zone1TimerWednesday": ( "z1Timer.Wednesday", None, "mdi:timer-outline", 1, None, - ], - "Zone1TimerThursday": ["z1Timer.Thursday", None, "mdi:timer-outline", 1, None], - "Zone1TimerFriday": ["z1Timer.Friday", None, "mdi:timer-outline", 1, None], - "Zone1TimerSaturday": ["z1Timer.Saturday", None, "mdi:timer-outline", 1, None], - "Zone1TimerSunday": ["z1Timer.Sunday", None, "mdi:timer-outline", 1, None], - "Zone1OperativeMode": ["z1OpMode", None, "mdi:math-compass", 3, None], - "ContinuosHeating": [ + ), + "Zone1TimerThursday": ("z1Timer.Thursday", None, "mdi:timer-outline", 1, None), + "Zone1TimerFriday": ("z1Timer.Friday", None, "mdi:timer-outline", 1, None), + "Zone1TimerSaturday": ("z1Timer.Saturday", None, "mdi:timer-outline", 1, None), + "Zone1TimerSunday": ("z1Timer.Sunday", None, "mdi:timer-outline", 1, None), + "Zone1OperativeMode": ("z1OpMode", None, "mdi:math-compass", 3, None), + "ContinuosHeating": ( "ContinuosHeating", UnitOfTemperature.CELSIUS, "mdi:weather-snowy", 0, SensorDeviceClass.TEMPERATURE, - ], - "PowerEnergyConsumptionLastMonth": [ + ), + "PowerEnergyConsumptionLastMonth": ( "PrEnergySumHcLastMonth", UnitOfEnergy.KILO_WATT_HOUR, "mdi:flash", 0, SensorDeviceClass.ENERGY, - ], - "PowerEnergyConsumptionThisMonth": [ + ), + "PowerEnergyConsumptionThisMonth": ( "PrEnergySumHcThisMonth", UnitOfEnergy.KILO_WATT_HOUR, "mdi:flash", 0, SensorDeviceClass.ENERGY, - ], + ), }, "ehp": { - "HWTemperature": [ + "HWTemperature": ( "HwcTemp", UnitOfTemperature.CELSIUS, None, 4, SensorDeviceClass.TEMPERATURE, - ], - "OutsideTemp": [ + ), + "OutsideTemp": ( "OutsideTemp", UnitOfTemperature.CELSIUS, None, 4, SensorDeviceClass.TEMPERATURE, - ], + ), }, "bai": { - "HotWaterTemperature": [ + "HotWaterTemperature": ( "HwcTemp", UnitOfTemperature.CELSIUS, None, 4, SensorDeviceClass.TEMPERATURE, - ], - "StorageTemperature": [ + ), + "StorageTemperature": ( "StorageTemp", UnitOfTemperature.CELSIUS, None, 4, SensorDeviceClass.TEMPERATURE, - ], - "DesiredStorageTemperature": [ + ), + "DesiredStorageTemperature": ( "StorageTempDesired", UnitOfTemperature.CELSIUS, None, 0, SensorDeviceClass.TEMPERATURE, - ], - "OutdoorsTemperature": [ + ), + "OutdoorsTemperature": ( "OutdoorstempSensor", UnitOfTemperature.CELSIUS, None, 4, SensorDeviceClass.TEMPERATURE, - ], - "WaterPressure": [ + ), + "WaterPressure": ( "WaterPressure", UnitOfPressure.BAR, "mdi:pipe", 4, SensorDeviceClass.PRESSURE, - ], - "AverageIgnitionTime": [ + ), + "AverageIgnitionTime": ( "averageIgnitiontime", UnitOfTime.SECONDS, "mdi:av-timer", 0, SensorDeviceClass.DURATION, - ], - "MaximumIgnitionTime": [ + ), + "MaximumIgnitionTime": ( "maxIgnitiontime", UnitOfTime.SECONDS, "mdi:av-timer", 0, SensorDeviceClass.DURATION, - ], - "MinimumIgnitionTime": [ + ), + "MinimumIgnitionTime": ( "minIgnitiontime", UnitOfTime.SECONDS, "mdi:av-timer", 0, SensorDeviceClass.DURATION, - ], - "ReturnTemperature": [ + ), + "ReturnTemperature": ( "ReturnTemp", UnitOfTemperature.CELSIUS, None, 4, SensorDeviceClass.TEMPERATURE, - ], - "CentralHeatingPump": ["WP", None, "mdi:toggle-switch", 2, None], - "HeatingSwitch": ["HeatingSwitch", None, "mdi:toggle-switch", 2, None], - "DesiredFlowTemperature": [ + ), + "CentralHeatingPump": ("WP", None, "mdi:toggle-switch", 2, None), + "HeatingSwitch": ("HeatingSwitch", None, "mdi:toggle-switch", 2, None), + "DesiredFlowTemperature": ( "FlowTempDesired", UnitOfTemperature.CELSIUS, None, 0, SensorDeviceClass.TEMPERATURE, - ], - "FlowTemperature": [ + ), + "FlowTemperature": ( "FlowTemp", UnitOfTemperature.CELSIUS, None, 4, SensorDeviceClass.TEMPERATURE, - ], - "Flame": ["Flame", None, "mdi:toggle-switch", 2, None], - "PowerEnergyConsumptionHeatingCircuit": [ + ), + "Flame": ("Flame", None, "mdi:toggle-switch", 2, None), + "PowerEnergyConsumptionHeatingCircuit": ( "PrEnergySumHc1", UnitOfEnergy.KILO_WATT_HOUR, "mdi:flash", 0, SensorDeviceClass.ENERGY, - ], - "PowerEnergyConsumptionHotWaterCircuit": [ + ), + "PowerEnergyConsumptionHotWaterCircuit": ( "PrEnergySumHwc1", UnitOfEnergy.KILO_WATT_HOUR, "mdi:flash", 0, SensorDeviceClass.ENERGY, - ], - "RoomThermostat": ["DCRoomthermostat", None, "mdi:toggle-switch", 2, None], - "HeatingPartLoad": [ + ), + "RoomThermostat": ("DCRoomthermostat", None, "mdi:toggle-switch", 2, None), + "HeatingPartLoad": ( "PartloadHcKW", UnitOfEnergy.KILO_WATT_HOUR, "mdi:flash", 0, SensorDeviceClass.ENERGY, - ], - "StateNumber": ["StateNumber", None, "mdi:fire", 3, None], - "ModulationPercentage": [ + ), + "StateNumber": ("StateNumber", None, "mdi:fire", 3, None), + "ModulationPercentage": ( "ModulationTempDesired", PERCENTAGE, "mdi:percent", 0, None, - ], + ), }, } diff --git a/homeassistant/components/ebusd/sensor.py b/homeassistant/components/ebusd/sensor.py index 0db0334f94f2ba..a69a0343220162 100644 --- a/homeassistant/components/ebusd/sensor.py +++ b/homeassistant/components/ebusd/sensor.py @@ -4,6 +4,7 @@ import datetime import logging +from typing import Any, cast from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.core import HomeAssistant @@ -11,7 +12,8 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import Throttle, dt as dt_util -from .const import DOMAIN +from . import EbusdData +from .const import EBUSD_DATA, SensorSpecs TIME_FRAME1_BEGIN = "time_frame1_begin" TIME_FRAME1_END = "time_frame1_end" @@ -33,9 +35,9 @@ def setup_platform( """Set up the Ebus sensor.""" if not discovery_info: return - ebusd_api = hass.data[DOMAIN] - monitored_conditions = discovery_info["monitored_conditions"] - name = discovery_info["client_name"] + ebusd_api = hass.data[EBUSD_DATA] + monitored_conditions: list[str] = discovery_info["monitored_conditions"] + name: str = discovery_info["client_name"] add_entities( ( @@ -49,9 +51,8 @@ def setup_platform( class EbusdSensor(SensorEntity): """Ebusd component sensor methods definition.""" - def __init__(self, data, sensor, name): + def __init__(self, data: EbusdData, sensor: SensorSpecs, name: str) -> None: """Initialize the sensor.""" - self._state = None self._client_name = name ( self._name, @@ -63,20 +64,15 @@ def __init__(self, data, sensor, name): self.data = data @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return f"{self._client_name} {self._name}" @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the device state attributes.""" - if self._type == 1 and self._state is not None: - schedule = { + if self._type == 1 and (native_value := self.native_value) is not None: + schedule: dict[str, str | None] = { TIME_FRAME1_BEGIN: None, TIME_FRAME1_END: None, TIME_FRAME2_BEGIN: None, @@ -84,7 +80,7 @@ def extra_state_attributes(self): TIME_FRAME3_BEGIN: None, TIME_FRAME3_END: None, } - time_frame = self._state.split(";") + time_frame = cast(str, native_value).split(";") for index, item in enumerate(sorted(schedule.items())): if index < len(time_frame): parsed = datetime.datetime.strptime(time_frame[index], "%H:%M") @@ -101,12 +97,12 @@ def device_class(self) -> SensorDeviceClass | None: return self._device_class @property - def icon(self): + def icon(self) -> str | None: """Icon to use in the frontend, if any.""" return self._icon @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement.""" return self._unit_of_measurement @@ -118,6 +114,6 @@ def update(self) -> None: if self._name not in self.data.value: return - self._state = self.data.value[self._name] + self._attr_native_value = self.data.value[self._name] except RuntimeError: _LOGGER.debug("EbusdData.update exception") diff --git a/homeassistant/components/hue/v1/sensor_base.py b/homeassistant/components/hue/v1/sensor_base.py index fb8f3c572c1227..0ea079992e004c 100644 --- a/homeassistant/components/hue/v1/sensor_base.py +++ b/homeassistant/components/hue/v1/sensor_base.py @@ -181,7 +181,7 @@ def available(self): ) @property - def state_class(self): + def state_class(self) -> SensorStateClass: """Return the state class of this entity, from STATE_CLASSES, if any.""" return SensorStateClass.MEASUREMENT diff --git a/homeassistant/components/mfi/sensor.py b/homeassistant/components/mfi/sensor.py index 6e792fe16091ef..b46d876cd514c2 100644 --- a/homeassistant/components/mfi/sensor.py +++ b/homeassistant/components/mfi/sensor.py @@ -4,7 +4,7 @@ import logging -from mficlient.client import FailedToLogin, MFiClient +from mficlient.client import FailedToLogin, MFiClient, Port as MFiPort import requests import voluptuous as vol @@ -12,6 +12,7 @@ PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, + StateType, ) from homeassistant.const import ( CONF_HOST, @@ -64,24 +65,29 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up mFi sensors.""" - host = config.get(CONF_HOST) - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - use_tls = config.get(CONF_SSL) - verify_tls = config.get(CONF_VERIFY_SSL) + host: str = config[CONF_HOST] + username: str = config[CONF_USERNAME] + password: str = config[CONF_PASSWORD] + use_tls: bool = config[CONF_SSL] + verify_tls: bool = config[CONF_VERIFY_SSL] default_port = 6443 if use_tls else 6080 - port = int(config.get(CONF_PORT, default_port)) + network_port: int = config.get(CONF_PORT, default_port) try: client = MFiClient( - host, username, password, port=port, use_tls=use_tls, verify=verify_tls + host, + username, + password, + port=network_port, + use_tls=use_tls, + verify=verify_tls, ) except (FailedToLogin, requests.exceptions.ConnectionError) as ex: _LOGGER.error("Unable to connect to mFi: %s", str(ex)) return add_entities( - MfiSensor(port, hass) + MfiSensor(port) for device in client.get_devices() for port in device.ports.values() if port.model in SENSOR_MODELS @@ -91,18 +97,17 @@ def setup_platform( class MfiSensor(SensorEntity): """Representation of a mFi sensor.""" - def __init__(self, port, hass): + def __init__(self, port: MFiPort) -> None: """Initialize the sensor.""" self._port = port - self._hass = hass @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return self._port.label @property - def native_value(self): + def native_value(self) -> StateType: """Return the state of the sensor.""" try: tag = self._port.tag @@ -129,7 +134,7 @@ def device_class(self) -> SensorDeviceClass | None: return None @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity, if any.""" try: tag = self._port.tag diff --git a/homeassistant/components/mfi/switch.py b/homeassistant/components/mfi/switch.py index 2a05018f301227..1fbf7f8cb826b6 100644 --- a/homeassistant/components/mfi/switch.py +++ b/homeassistant/components/mfi/switch.py @@ -5,7 +5,7 @@ import logging from typing import Any -from mficlient.client import FailedToLogin, MFiClient +from mficlient.client import FailedToLogin, MFiClient, Port as MFiPort import requests import voluptuous as vol @@ -51,18 +51,23 @@ def setup_platform( add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up mFi sensors.""" - host = config.get(CONF_HOST) - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - use_tls = config[CONF_SSL] - verify_tls = config.get(CONF_VERIFY_SSL) + """Set up mFi switches.""" + host: str = config[CONF_HOST] + username: str = config[CONF_USERNAME] + password: str = config[CONF_PASSWORD] + use_tls: bool = config[CONF_SSL] + verify_tls: bool = config[CONF_VERIFY_SSL] default_port = 6443 if use_tls else 6080 - port = int(config.get(CONF_PORT, default_port)) + network_port: int = config.get(CONF_PORT, default_port) try: client = MFiClient( - host, username, password, port=port, use_tls=use_tls, verify=verify_tls + host, + username, + password, + port=network_port, + use_tls=use_tls, + verify=verify_tls, ) except (FailedToLogin, requests.exceptions.ConnectionError) as ex: _LOGGER.error("Unable to connect to mFi: %s", str(ex)) @@ -79,23 +84,23 @@ def setup_platform( class MfiSwitch(SwitchEntity): """Representation of an mFi switch-able device.""" - def __init__(self, port): + def __init__(self, port: MFiPort) -> None: """Initialize the mFi device.""" self._port = port - self._target_state = None + self._target_state: bool | None = None @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique ID of the device.""" return self._port.ident @property - def name(self): + def name(self) -> str: """Return the name of the device.""" return self._port.label @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if the device is on.""" return self._port.output diff --git a/homeassistant/components/onedrive/manifest.json b/homeassistant/components/onedrive/manifest.json index df861f99751bca..982fceb1e04458 100644 --- a/homeassistant/components/onedrive/manifest.json +++ b/homeassistant/components/onedrive/manifest.json @@ -10,5 +10,5 @@ "iot_class": "cloud_polling", "loggers": ["onedrive_personal_sdk"], "quality_scale": "platinum", - "requirements": ["onedrive-personal-sdk==0.0.17"] + "requirements": ["onedrive-personal-sdk==0.1.0"] } diff --git a/homeassistant/components/openai_conversation/config_flow.py b/homeassistant/components/openai_conversation/config_flow.py index cdfd3b72cfcd3f..426ee7d1945c52 100644 --- a/homeassistant/components/openai_conversation/config_flow.py +++ b/homeassistant/components/openai_conversation/config_flow.py @@ -112,45 +112,49 @@ async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle the initial step.""" - if user_input is None: - return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA - ) errors: dict[str, str] = {} - self._async_abort_entries_match(user_input) - try: - await validate_input(self.hass, user_input) - except openai.APIConnectionError: - errors["base"] = "cannot_connect" - except openai.AuthenticationError: - errors["base"] = "invalid_auth" - except Exception: - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - else: - return self.async_create_entry( - title="ChatGPT", - data=user_input, - subentries=[ - { - "subentry_type": "conversation", - "data": RECOMMENDED_CONVERSATION_OPTIONS, - "title": DEFAULT_CONVERSATION_NAME, - "unique_id": None, - }, - { - "subentry_type": "ai_task_data", - "data": RECOMMENDED_AI_TASK_OPTIONS, - "title": DEFAULT_AI_TASK_NAME, - "unique_id": None, - }, - ], - ) + if user_input is not None: + self._async_abort_entries_match(user_input) + try: + await validate_input(self.hass, user_input) + except openai.APIConnectionError: + errors["base"] = "cannot_connect" + except openai.AuthenticationError: + errors["base"] = "invalid_auth" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry( + title="ChatGPT", + data=user_input, + subentries=[ + { + "subentry_type": "conversation", + "data": RECOMMENDED_CONVERSATION_OPTIONS, + "title": DEFAULT_CONVERSATION_NAME, + "unique_id": None, + }, + { + "subentry_type": "ai_task_data", + "data": RECOMMENDED_AI_TASK_OPTIONS, + "title": DEFAULT_AI_TASK_NAME, + "unique_id": None, + }, + ], + ) return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + step_id="user", + data_schema=self.add_suggested_values_to_schema( + STEP_USER_DATA_SCHEMA, user_input + ), + errors=errors, + description_placeholders={ + "instructions_url": "https://www.home-assistant.io/integrations/openai_conversation/#generate-an-api-key", + }, ) @classmethod diff --git a/homeassistant/components/openai_conversation/strings.json b/homeassistant/components/openai_conversation/strings.json index 4b870d23c30b59..a5f283f871227e 100644 --- a/homeassistant/components/openai_conversation/strings.json +++ b/homeassistant/components/openai_conversation/strings.json @@ -12,7 +12,11 @@ "user": { "data": { "api_key": "[%key:common::config_flow::data::api_key%]" - } + }, + "data_description": { + "api_key": "Your OpenAI API key." + }, + "description": "Set up OpenAI Conversation integration by providing your OpenAI API key. Instructions to obtain an API key can be found [here]({instructions_url})." } } }, diff --git a/homeassistant/components/teslemetry/__init__.py b/homeassistant/components/teslemetry/__init__.py index 26b7533b3d9805..42d0ac25388b59 100644 --- a/homeassistant/components/teslemetry/__init__.py +++ b/homeassistant/components/teslemetry/__init__.py @@ -2,10 +2,10 @@ import asyncio from collections.abc import Callable +from functools import partial from typing import Final -from aiohttp import ClientResponseError -from aiohttp.client_exceptions import ClientError +from aiohttp import ClientError, ClientResponseError from tesla_fleet_api.const import Scope from tesla_fleet_api.exceptions import ( Forbidden, @@ -27,6 +27,7 @@ from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.config_entry_oauth2_flow import ( + ImplementationUnavailableError, OAuth2Session, async_get_config_entry_implementation, ) @@ -75,28 +76,48 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True +async def _get_access_token(oauth_session: OAuth2Session) -> str: + """Get a valid access token, refreshing if necessary.""" + LOGGER.debug( + "Token valid: %s, expires_at: %s", + oauth_session.valid_token, + oauth_session.token.get("expires_at"), + ) + try: + await oauth_session.async_ensure_token_valid() + except ClientResponseError as err: + if err.status == 401: + raise ConfigEntryAuthFailed from err + raise ConfigEntryNotReady from err + except (KeyError, TypeError) as err: + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="token_data_malformed", + ) from err + except ClientError as err: + raise ConfigEntryNotReady from err + return oauth_session.token[CONF_ACCESS_TOKEN] + + async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -> bool: """Set up Teslemetry config.""" session = async_get_clientsession(hass) - implementation = await async_get_config_entry_implementation(hass, entry) + try: + implementation = await async_get_config_entry_implementation(hass, entry) + except ImplementationUnavailableError as err: + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="oauth_implementation_not_available", + ) from err oauth_session = OAuth2Session(hass, entry, implementation) - async def _get_access_token() -> str: - try: - await oauth_session.async_ensure_token_valid() - except ClientResponseError as e: - if e.status == 401: - raise ConfigEntryAuthFailed from e - raise ConfigEntryNotReady from e - token: str = oauth_session.token[CONF_ACCESS_TOKEN] - return token - # Create API connection + access_token = partial(_get_access_token, oauth_session) teslemetry = Teslemetry( session=session, - access_token=_get_access_token, + access_token=access_token, ) try: calls = await asyncio.gather( @@ -154,7 +175,7 @@ async def _get_access_token() -> str: if not stream: stream = TeslemetryStream( session, - _get_access_token, + access_token, server=f"{region.lower()}.teslemetry.com", parse_timestamp=True, manual=True, diff --git a/homeassistant/components/teslemetry/strings.json b/homeassistant/components/teslemetry/strings.json index f99c6f72e206e7..6eb27ae24c42de 100644 --- a/homeassistant/components/teslemetry/strings.json +++ b/homeassistant/components/teslemetry/strings.json @@ -997,7 +997,6 @@ "total_grid_energy_exported": { "name": "Grid exported" }, - "total_home_usage": { "name": "Home usage" }, @@ -1127,6 +1126,9 @@ "no_vehicle_data_for_device": { "message": "No vehicle data for device ID: {device_id}" }, + "oauth_implementation_not_available": { + "message": "OAuth implementation not available, try reauthenticating" + }, "set_scheduled_charging_time": { "message": "Scheduled charging time is required when enabling" }, @@ -1136,6 +1138,9 @@ "set_scheduled_departure_preconditioning": { "message": "Preconditioning departure time is required when enabling" }, + "token_data_malformed": { + "message": "Token data malformed, try reauthenticating" + }, "wake_up_failed": { "message": "Failed to wake up vehicle: {message}" }, diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index dd0e62b0dbd39b..071488dc68b495 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -700,7 +700,7 @@ def device_class(self) -> SensorDeviceClass | None: return None @property - def state_class(self): + def state_class(self) -> SensorStateClass: """Return the device class of the sensor.""" return ( SensorStateClass.TOTAL diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index b4f7a5a4a5a99b..a562ea69d6848c 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -700,6 +700,7 @@ class ClassTypeHintMatch: TypeHintMatch( function_name="device_class", return_type=["str", None], + mandatory=True, ), TypeHintMatch( function_name="unit_of_measurement", @@ -2518,10 +2519,12 @@ class ClassTypeHintMatch: TypeHintMatch( function_name="state_class", return_type=["SensorStateClass", "str", None], + mandatory=True, ), TypeHintMatch( function_name="last_reset", return_type=["datetime", None], + mandatory=True, ), TypeHintMatch( function_name="native_value", diff --git a/requirements_all.txt b/requirements_all.txt index f6af43716e04f8..e3858be0ed0c25 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1646,7 +1646,7 @@ omnilogic==0.4.5 ondilo==0.5.0 # homeassistant.components.onedrive -onedrive-personal-sdk==0.0.17 +onedrive-personal-sdk==0.1.0 # homeassistant.components.onvif onvif-zeep-async==4.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b337ec21372f15..b58f997f0d57bc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1429,7 +1429,7 @@ omnilogic==0.4.5 ondilo==0.5.0 # homeassistant.components.onedrive -onedrive-personal-sdk==0.0.17 +onedrive-personal-sdk==0.1.0 # homeassistant.components.onvif onvif-zeep-async==4.0.4 diff --git a/tests/components/mfi/test_sensor.py b/tests/components/mfi/test_sensor.py index 8c21fa9cb36e4a..b756fb44a727e1 100644 --- a/tests/components/mfi/test_sensor.py +++ b/tests/components/mfi/test_sensor.py @@ -34,7 +34,7 @@ async def test_setup_missing_config(hass: HomeAssistant) -> None: """Test setup with missing configuration.""" with mock.patch("homeassistant.components.mfi.sensor.MFiClient") as mock_client: config = {"sensor": {"platform": "mfi"}} - assert await async_setup_component(hass, "sensor", config) + assert await async_setup_component(hass, COMPONENT.DOMAIN, config) assert not mock_client.called @@ -42,14 +42,14 @@ async def test_setup_failed_login(hass: HomeAssistant) -> None: """Test setup with login failure.""" with mock.patch("homeassistant.components.mfi.sensor.MFiClient") as mock_client: mock_client.side_effect = FailedToLogin - assert not PLATFORM.setup_platform(hass, GOOD_CONFIG, None) + assert not PLATFORM.setup_platform(hass, GOOD_CONFIG[COMPONENT.DOMAIN], None) async def test_setup_failed_connect(hass: HomeAssistant) -> None: """Test setup with connection failure.""" with mock.patch("homeassistant.components.mfi.sensor.MFiClient") as mock_client: mock_client.side_effect = requests.exceptions.ConnectionError - assert not PLATFORM.setup_platform(hass, GOOD_CONFIG, None) + assert not PLATFORM.setup_platform(hass, GOOD_CONFIG[COMPONENT.DOMAIN], None) async def test_setup_minimum(hass: HomeAssistant) -> None: @@ -111,7 +111,7 @@ async def test_setup_adds_proper_devices(hass: HomeAssistant) -> None: await hass.async_block_till_done() for ident, port in ports.items(): if ident != "bad": - mock_sensor.assert_any_call(port, hass) + mock_sensor.assert_any_call(port) assert mock.call(ports["bad"], hass) not in mock_sensor.mock_calls @@ -124,7 +124,7 @@ def port_fixture() -> mock.MagicMock: @pytest.fixture(name="sensor") def sensor_fixture(hass: HomeAssistant, port: mock.MagicMock) -> mfi.MfiSensor: """Sensor fixture.""" - sensor = mfi.MfiSensor(port, hass) + sensor = mfi.MfiSensor(port) sensor.hass = hass return sensor diff --git a/tests/components/openai_conversation/test_config_flow.py b/tests/components/openai_conversation/test_config_flow.py index 202514a77b5d31..20fae149c2a0b7 100644 --- a/tests/components/openai_conversation/test_config_flow.py +++ b/tests/components/openai_conversation/test_config_flow.py @@ -58,7 +58,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM - assert result["errors"] is None + assert result["errors"] == {} with ( patch( diff --git a/tests/components/teslemetry/test_init.py b/tests/components/teslemetry/test_init.py index 949a3c29829696..bb6ca8a4ed2343 100644 --- a/tests/components/teslemetry/test_init.py +++ b/tests/components/teslemetry/test_init.py @@ -443,6 +443,37 @@ async def test_migrate_from_future_version_fails(hass: HomeAssistant) -> None: assert entry.version == 3 # Version should remain unchanged +async def test_oauth_implementation_not_available(hass: HomeAssistant) -> None: + """Test that missing OAuth implementation triggers reauth.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + version=2, + unique_id=UNIQUE_ID, + data={ + "auth_implementation": DOMAIN, + "token": { + "access_token": "test_access_token", + "refresh_token": "test_refresh_token", + "expires_at": int(time.time()) + 3600, + }, + }, + ) + mock_entry.add_to_hass(hass) + + # Mock the implementation lookup to raise ValueError + with patch( + "homeassistant.components.teslemetry.async_get_config_entry_implementation", + side_effect=ValueError("Implementation not available"), + ): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + entry = hass.config_entries.async_get_entry(mock_entry.entry_id) + assert entry is not None + # Should trigger reauth, not just fail silently + assert entry.state is ConfigEntryState.SETUP_ERROR + + RETRY_EXCEPTIONS = [ (RateLimited(data={"after": 5}), 5.0), (InvalidResponse(), 10.0),