diff --git a/.strict-typing b/.strict-typing index e950da8d25d0b3..291c3d78e678b1 100644 --- a/.strict-typing +++ b/.strict-typing @@ -555,6 +555,7 @@ homeassistant.components.vacuum.* homeassistant.components.vallox.* homeassistant.components.valve.* homeassistant.components.velbus.* +homeassistant.components.vivotek.* homeassistant.components.vlc_telnet.* homeassistant.components.vodafone_station.* homeassistant.components.volvo.* diff --git a/CODEOWNERS b/CODEOWNERS index ccd8cbadb6bdf1..3235a5b73dff0c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1196,8 +1196,6 @@ build.json @home-assistant/supervisor /tests/components/plex/ @jjlawren /homeassistant/components/plugwise/ @CoMPaTech @bouwew /tests/components/plugwise/ @CoMPaTech @bouwew -/homeassistant/components/plum_lightpad/ @ColinHarrington @prystupa -/tests/components/plum_lightpad/ @ColinHarrington @prystupa /homeassistant/components/point/ @fredrike /tests/components/point/ @fredrike /homeassistant/components/pooldose/ @lmaertin diff --git a/homeassistant/components/kraken/__init__.py b/homeassistant/components/kraken/__init__.py index 5c3158bddf2330..ccdd704d9df19d 100644 --- a/homeassistant/components/kraken/__init__.py +++ b/homeassistant/components/kraken/__init__.py @@ -147,8 +147,9 @@ async def async_setup(self) -> None: def _get_websocket_name_asset_pairs(self) -> str: return ",".join( - self.tradable_asset_pairs[tracked_pair] + pair for tracked_pair in self._config_entry.options[CONF_TRACKED_ASSET_PAIRS] + if (pair := self.tradable_asset_pairs.get(tracked_pair)) is not None ) def set_update_interval(self, update_interval: int) -> None: diff --git a/homeassistant/components/kraken/sensor.py b/homeassistant/components/kraken/sensor.py index 1d3f36d29e4194..2a6b36e17294bc 100644 --- a/homeassistant/components/kraken/sensor.py +++ b/homeassistant/components/kraken/sensor.py @@ -156,7 +156,7 @@ def _async_add_kraken_sensors(tracked_asset_pairs: list[str]) -> None: for description in SENSOR_TYPES ] ) - async_add_entities(entities, True) + async_add_entities(entities) _async_add_kraken_sensors(config_entry.options[CONF_TRACKED_ASSET_PAIRS]) diff --git a/homeassistant/components/oralb/icons.json b/homeassistant/components/oralb/icons.json new file mode 100644 index 00000000000000..ec8426d28a0f8f --- /dev/null +++ b/homeassistant/components/oralb/icons.json @@ -0,0 +1,61 @@ +{ + "entity": { + "sensor": { + "pressure": { + "default": "mdi:tooth-outline", + "state": { + "high": "mdi:tooth", + "low": "mdi:alert", + "power_button_pressed": "mdi:power", + "button_pressed": "mdi:radiobox-marked" + } + }, + "sector": { + "default": "mdi:circle-outline", + "state": { + "sector_1": "mdi:circle-slice-2", + "sector_2": "mdi:circle-slice-4", + "sector_3": "mdi:circle-slice-6", + "sector_4": "mdi:circle-slice-8", + "success": "mdi:check-circle-outline" + } + }, + "toothbrush_state": { + "default": "mdi:toothbrush-electric", + "state": { + "initializing": "mdi:sync", + "idle": "mdi:toothbrush-electric", + "running": "mdi:waveform", + "charging": "mdi:battery-charging", + "setup": "mdi:wrench", + "flight_menu": "mdi:airplane", + "selection_menu": "mdi:menu", + "off": "mdi:power", + "sleeping": "mdi:sleep", + "transport": "mdi:dolly" + } + }, + "number_of_sectors": { + "default": "mdi:chart-pie" + }, + "mode": { + "default": "mdi:toothbrush-paste", + "state": { + "daily_clean": "mdi:repeat-once", + "sensitive": "mdi:feather", + "gum_care": "mdi:tooth-outline", + "intense": "mdi:shape-circle-plus", + "whitening": "mdi:shimmer", + "whiten": "mdi:shimmer", + "tongue_cleaning": "mdi:gate-and", + "super_sensitive": "mdi:feather", + "massage": "mdi:spa", + "deep_clean": "mdi:water", + "turbo": "mdi:car-turbocharger", + "off": "mdi:power", + "settings": "mdi:cog-outline" + } + } + } + } +} diff --git a/homeassistant/components/oralb/sensor.py b/homeassistant/components/oralb/sensor.py index 3b345f4b36aa3e..17d68a6aaab6ef 100644 --- a/homeassistant/components/oralb/sensor.py +++ b/homeassistant/components/oralb/sensor.py @@ -3,6 +3,13 @@ from __future__ import annotations from oralb_ble import OralBSensor, SensorUpdate +from oralb_ble.parser import ( + IO_SERIES_MODES, + PRESSURE, + SECTOR_MAP, + SMART_SERIES_MODES, + STATES, +) from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, @@ -39,6 +46,8 @@ key=OralBSensor.SECTOR, translation_key="sector", entity_category=EntityCategory.DIAGNOSTIC, + options=[v.replace(" ", "_") for v in set(SECTOR_MAP.values()) | {"no_sector"}], + device_class=SensorDeviceClass.ENUM, ), OralBSensor.NUMBER_OF_SECTORS: SensorEntityDescription( key=OralBSensor.NUMBER_OF_SECTORS, @@ -53,16 +62,26 @@ ), OralBSensor.TOOTHBRUSH_STATE: SensorEntityDescription( key=OralBSensor.TOOTHBRUSH_STATE, + translation_key="toothbrush_state", + options=[v.replace(" ", "_") for v in set(STATES.values())], + device_class=SensorDeviceClass.ENUM, name=None, ), OralBSensor.PRESSURE: SensorEntityDescription( key=OralBSensor.PRESSURE, translation_key="pressure", + options=[v.replace(" ", "_") for v in set(PRESSURE.values()) | {"low"}], + device_class=SensorDeviceClass.ENUM, ), OralBSensor.MODE: SensorEntityDescription( key=OralBSensor.MODE, translation_key="mode", entity_category=EntityCategory.DIAGNOSTIC, + options=[ + v.replace(" ", "_") + for v in set(IO_SERIES_MODES.values()) | set(SMART_SERIES_MODES.values()) + ], + device_class=SensorDeviceClass.ENUM, ), OralBSensor.SIGNAL_STRENGTH: SensorEntityDescription( key=OralBSensor.SIGNAL_STRENGTH, @@ -134,7 +153,15 @@ class OralBBluetoothSensorEntity( @property def native_value(self) -> str | int | None: """Return the native value.""" - return self.processor.entity_data.get(self.entity_key) + value = self.processor.entity_data.get(self.entity_key) + if isinstance(value, str): + value = value.replace(" ", "_") + if ( + self.entity_description.options is not None + and value not in self.entity_description.options + ): # append unknown values to enum + self.entity_description.options.append(value) + return value @property def available(self) -> bool: diff --git a/homeassistant/components/oralb/strings.json b/homeassistant/components/oralb/strings.json index 775bbedac74f0f..db3b8de5965e71 100644 --- a/homeassistant/components/oralb/strings.json +++ b/homeassistant/components/oralb/strings.json @@ -22,7 +22,15 @@ "entity": { "sensor": { "sector": { - "name": "Sector" + "name": "Sector", + "state": { + "no_sector": "No sector", + "sector_1": "Sector 1", + "sector_2": "Sector 2", + "sector_3": "Sector 3", + "sector_4": "Sector 4", + "success": "Success" + } }, "number_of_sectors": { "name": "Number of sectors" @@ -31,10 +39,48 @@ "name": "Sector timer" }, "pressure": { - "name": "Pressure" + "name": "Pressure", + "state": { + "normal": "[%key:common::state::normal%]", + "high": "[%key:common::state::high%]", + "low": "[%key:common::state::low%]", + "power_button_pressed": "Power button pressed", + "button_pressed": "Button pressed" + } }, "mode": { - "name": "Brushing mode" + "name": "Brushing mode", + "state": { + "daily_clean": "Daily clean", + "sensitive": "Sensitive", + "gum_care": "Gum care", + "intense": "Intense", + "whitening": "Whiten", + "whiten": "[%key:component::oralb::entity::sensor::mode::state::whitening%]", + "tongue_cleaning": "Tongue clean", + "super_sensitive": "Super sensitive", + "massage": "Massage", + "deep_clean": "Deep clean", + "turbo": "Turbo", + "off": "[%key:common::state::off%]", + "settings": "Settings" + } + }, + "toothbrush_state": { + "state": { + "initializing": "Initializing", + "idle": "[%key:common::state::idle%]", + "running": "Running", + "charging": "[%key:common::state::charging%]", + "setup": "Setup", + "flight_menu": "Flight menu", + "selection_menu": "Selection menu", + "off": "[%key:common::state::off%]", + "sleeping": "Sleeping", + "transport": "Transport", + "final_test": "Final test", + "pcb_test": "PCB test" + } } } } diff --git a/homeassistant/components/plum_lightpad/__init__.py b/homeassistant/components/plum_lightpad/__init__.py index f1816f03d3b6f4..831f50b1a9ee63 100644 --- a/homeassistant/components/plum_lightpad/__init__.py +++ b/homeassistant/components/plum_lightpad/__init__.py @@ -1,52 +1,36 @@ """Support for Plum Lightpad devices.""" -import logging - -from aiohttp import ContentTypeError -from requests.exceptions import ConnectTimeout, HTTPError - -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_PASSWORD, - CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP, - Platform, -) +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import issue_registry as ir -from .const import DOMAIN -from .utils import load_plum +DOMAIN = "plum_lightpad" -_LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.LIGHT] - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool: """Set up Plum Lightpad from a config entry.""" - _LOGGER.debug("Setting up config entry with ID = %s", entry.unique_id) - - username = entry.data[CONF_USERNAME] - password = entry.data[CONF_PASSWORD] + ir.async_create_issue( + hass, + DOMAIN, + DOMAIN, + is_fixable=False, + severity=ir.IssueSeverity.ERROR, + translation_key="integration_removed", + translation_placeholders={ + "entries": "/config/integrations/integration/plum_lightpad", + }, + ) - try: - plum = await load_plum(username, password, hass) - except ContentTypeError as ex: - _LOGGER.error("Unable to authenticate to Plum cloud: %s", ex) - return False - except (ConnectTimeout, HTTPError) as ex: - _LOGGER.error("Unable to connect to Plum cloud: %s", ex) - raise ConfigEntryNotReady from ex - - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = plum + return True - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - def cleanup(event): - """Clean up resources.""" - plum.cleanup() +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload config entry.""" + if all( + config_entry.state is ConfigEntryState.NOT_LOADED + for config_entry in hass.config_entries.async_entries(DOMAIN) + if config_entry.entry_id != entry.entry_id + ): + ir.async_delete_issue(hass, DOMAIN, DOMAIN) - entry.async_on_unload(hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup)) return True diff --git a/homeassistant/components/plum_lightpad/config_flow.py b/homeassistant/components/plum_lightpad/config_flow.py index 2a929d14c9e580..4a0b849d9397e6 100644 --- a/homeassistant/components/plum_lightpad/config_flow.py +++ b/homeassistant/components/plum_lightpad/config_flow.py @@ -2,59 +2,12 @@ from __future__ import annotations -import logging -from typing import Any +from homeassistant.config_entries import ConfigFlow -from aiohttp import ContentTypeError -from requests.exceptions import ConnectTimeout, HTTPError -import voluptuous as vol - -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME - -from .const import DOMAIN -from .utils import load_plum - -_LOGGER = logging.getLogger(__name__) +from . import DOMAIN class PlumLightpadConfigFlow(ConfigFlow, domain=DOMAIN): """Config flow for Plum Lightpad integration.""" VERSION = 1 - - def _show_form(self, errors=None): - schema = { - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - } - - return self.async_show_form( - step_id="user", - data_schema=vol.Schema(schema), - errors=errors or {}, - ) - - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Handle a flow initialized by the user or redirected to by import.""" - if not user_input: - return self._show_form() - - username = user_input[CONF_USERNAME] - password = user_input[CONF_PASSWORD] - - # load Plum just so we know username/password work - try: - await load_plum(username, password, self.hass) - except (ContentTypeError, ConnectTimeout, HTTPError) as ex: - _LOGGER.error("Unable to connect/authenticate to Plum cloud: %s", str(ex)) - return self._show_form({"base": "cannot_connect"}) - - await self.async_set_unique_id(username) - self._abort_if_unique_id_configured() - - return self.async_create_entry( - title=username, data={CONF_USERNAME: username, CONF_PASSWORD: password} - ) diff --git a/homeassistant/components/plum_lightpad/const.py b/homeassistant/components/plum_lightpad/const.py deleted file mode 100644 index efea35d0a7a934..00000000000000 --- a/homeassistant/components/plum_lightpad/const.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Constants for the Plum Lightpad component.""" - -DOMAIN = "plum_lightpad" diff --git a/homeassistant/components/plum_lightpad/icons.json b/homeassistant/components/plum_lightpad/icons.json deleted file mode 100644 index dd65160e4744e5..00000000000000 --- a/homeassistant/components/plum_lightpad/icons.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "entity": { - "light": { - "glow_ring": { - "default": "mdi:crop-portrait" - } - } - } -} diff --git a/homeassistant/components/plum_lightpad/light.py b/homeassistant/components/plum_lightpad/light.py deleted file mode 100644 index 78743c1280847e..00000000000000 --- a/homeassistant/components/plum_lightpad/light.py +++ /dev/null @@ -1,201 +0,0 @@ -"""Support for Plum Lightpad lights.""" - -from __future__ import annotations - -from typing import Any - -from plumlightpad import Plum - -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - ATTR_HS_COLOR, - ColorMode, - LightEntity, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.util import color as color_util - -from .const import DOMAIN - - -async def async_setup_entry( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: AddConfigEntryEntitiesCallback, -) -> None: - """Set up Plum Lightpad dimmer lights and glow rings.""" - - plum: Plum = hass.data[DOMAIN][entry.entry_id] - - def setup_entities(device) -> None: - entities: list[LightEntity] = [] - - if "lpid" in device: - lightpad = plum.get_lightpad(device["lpid"]) - entities.append(GlowRing(lightpad=lightpad)) - - if "llid" in device: - logical_load = plum.get_load(device["llid"]) - entities.append(PlumLight(load=logical_load)) - - async_add_entities(entities) - - async def new_load(device): - setup_entities(device) - - async def new_lightpad(device): - setup_entities(device) - - device_web_session = async_get_clientsession(hass, verify_ssl=False) - entry.async_create_background_task( - hass, - plum.discover( - hass.loop, - loadListener=new_load, - lightpadListener=new_lightpad, - websession=device_web_session, - ), - "plum.light-discover", - ) - - -class PlumLight(LightEntity): - """Representation of a Plum Lightpad dimmer.""" - - _attr_should_poll = False - _attr_has_entity_name = True - _attr_name = None - - def __init__(self, load): - """Initialize the light.""" - self._load = load - self._brightness = load.level - unique_id = f"{load.llid}.light" - self._attr_unique_id = unique_id - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, unique_id)}, - manufacturer="Plum", - model="Dimmer", - name=load.name, - ) - - async def async_added_to_hass(self) -> None: - """Subscribe to dimmerchange events.""" - self._load.add_event_listener("dimmerchange", self.dimmerchange) - - def dimmerchange(self, event): - """Change event handler updating the brightness.""" - self._brightness = event["level"] - self.schedule_update_ha_state() - - @property - def brightness(self) -> int: - """Return the brightness of this switch between 0..255.""" - return self._brightness - - @property - def is_on(self) -> bool: - """Return true if light is on.""" - return self._brightness > 0 - - @property - def color_mode(self) -> ColorMode: - """Flag supported features.""" - if self._load.dimmable: - return ColorMode.BRIGHTNESS - return ColorMode.ONOFF - - @property - def supported_color_modes(self) -> set[ColorMode]: - """Flag supported color modes.""" - return {self.color_mode} - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the light on.""" - if ATTR_BRIGHTNESS in kwargs: - await self._load.turn_on(kwargs[ATTR_BRIGHTNESS]) - else: - await self._load.turn_on() - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the light off.""" - await self._load.turn_off() - - -class GlowRing(LightEntity): - """Representation of a Plum Lightpad dimmer glow ring.""" - - _attr_color_mode = ColorMode.HS - _attr_should_poll = False - _attr_translation_key = "glow_ring" - _attr_supported_color_modes = {ColorMode.HS} - - def __init__(self, lightpad): - """Initialize the light.""" - self._lightpad = lightpad - self._attr_name = f"{lightpad.friendly_name} Glow Ring" - - self._attr_is_on = lightpad.glow_enabled - self._glow_intensity = lightpad.glow_intensity - unique_id = f"{self._lightpad.lpid}.glow" - self._attr_unique_id = unique_id - - self._red = lightpad.glow_color["red"] - self._green = lightpad.glow_color["green"] - self._blue = lightpad.glow_color["blue"] - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, unique_id)}, - manufacturer="Plum", - model="Glow Ring", - name=self._attr_name, - ) - - async def async_added_to_hass(self) -> None: - """Subscribe to configchange events.""" - self._lightpad.add_event_listener("configchange", self.configchange_event) - - def configchange_event(self, event): - """Handle Configuration change event.""" - config = event["changes"] - - self._attr_is_on = config["glowEnabled"] - self._glow_intensity = config["glowIntensity"] - - self._red = config["glowColor"]["red"] - self._green = config["glowColor"]["green"] - self._blue = config["glowColor"]["blue"] - self.schedule_update_ha_state() - - @property - def hs_color(self): - """Return the hue and saturation color value [float, float].""" - return color_util.color_RGB_to_hs(self._red, self._green, self._blue) - - @property - def brightness(self) -> int: - """Return the brightness of this switch between 0..255.""" - return min(max(int(round(self._glow_intensity * 255, 0)), 0), 255) - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the light on.""" - if ATTR_BRIGHTNESS in kwargs: - brightness_pct = kwargs[ATTR_BRIGHTNESS] / 255.0 - await self._lightpad.set_config({"glowIntensity": brightness_pct}) - elif ATTR_HS_COLOR in kwargs: - hs_color = kwargs[ATTR_HS_COLOR] - red, green, blue = color_util.color_hs_to_RGB(*hs_color) - await self._lightpad.set_glow_color(red, green, blue, 0) - else: - await self._lightpad.set_config({"glowEnabled": True}) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the light off.""" - if ATTR_BRIGHTNESS in kwargs: - brightness_pct = kwargs[ATTR_BRIGHTNESS] / 255.0 - await self._lightpad.set_config({"glowIntensity": brightness_pct}) - else: - await self._lightpad.set_config({"glowEnabled": False}) diff --git a/homeassistant/components/plum_lightpad/manifest.json b/homeassistant/components/plum_lightpad/manifest.json index ffe2b47a0c6261..eee716d77e3bc3 100644 --- a/homeassistant/components/plum_lightpad/manifest.json +++ b/homeassistant/components/plum_lightpad/manifest.json @@ -1,10 +1,9 @@ { "domain": "plum_lightpad", "name": "Plum Lightpad", - "codeowners": ["@ColinHarrington", "@prystupa"], - "config_flow": true, + "codeowners": [], "documentation": "https://www.home-assistant.io/integrations/plum_lightpad", + "integration_type": "system", "iot_class": "local_push", - "loggers": ["plumlightpad"], - "requirements": ["plumlightpad==0.0.11"] + "requirements": [] } diff --git a/homeassistant/components/plum_lightpad/strings.json b/homeassistant/components/plum_lightpad/strings.json index 935e1614696023..d0268287d47704 100644 --- a/homeassistant/components/plum_lightpad/strings.json +++ b/homeassistant/components/plum_lightpad/strings.json @@ -1,18 +1,8 @@ { - "config": { - "step": { - "user": { - "data": { - "username": "[%key:common::config_flow::data::email%]", - "password": "[%key:common::config_flow::data::password%]" - } - } - }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + "issues": { + "integration_removed": { + "title": "The Plum Lightpad integration has been removed", + "description": "The Plum Lightpad integration has been removed from Home Assistant.\n\nThe required cloud services are no longer available since the Plum servers have been shut down. To resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing Plum Lightpad integration entries]({entries})." } } } diff --git a/homeassistant/components/plum_lightpad/utils.py b/homeassistant/components/plum_lightpad/utils.py deleted file mode 100644 index 6704b443d7284f..00000000000000 --- a/homeassistant/components/plum_lightpad/utils.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Reusable utilities for the Plum Lightpad component.""" - -from plumlightpad import Plum - -from homeassistant.core import HomeAssistant -from homeassistant.helpers.aiohttp_client import async_get_clientsession - - -async def load_plum(username: str, password: str, hass: HomeAssistant) -> Plum: - """Initialize Plum Lightpad API and load metadata stored in the cloud.""" - plum = Plum(username, password) - cloud_web_session = async_get_clientsession(hass, verify_ssl=True) - await plum.loadCloudData(cloud_web_session) - return plum diff --git a/homeassistant/components/vivotek/manifest.json b/homeassistant/components/vivotek/manifest.json index f0b622afcadaf0..74a8bf9b75047f 100644 --- a/homeassistant/components/vivotek/manifest.json +++ b/homeassistant/components/vivotek/manifest.json @@ -6,5 +6,5 @@ "iot_class": "local_polling", "loggers": ["libpyvivotek"], "quality_scale": "legacy", - "requirements": ["libpyvivotek==0.4.0"] + "requirements": ["libpyvivotek==0.6.1"] } diff --git a/homeassistant/components/wallbox/const.py b/homeassistant/components/wallbox/const.py index 1059a41db536f6..cbe1aaa912a9d4 100644 --- a/homeassistant/components/wallbox/const.py +++ b/homeassistant/components/wallbox/const.py @@ -3,7 +3,7 @@ from enum import StrEnum DOMAIN = "wallbox" -UPDATE_INTERVAL = 60 +UPDATE_INTERVAL = 90 BIDIRECTIONAL_MODEL_PREFIXES = ["QS"] diff --git a/homeassistant/components/wallbox/coordinator.py b/homeassistant/components/wallbox/coordinator.py index 4e743b2106b744..36785ee362a938 100644 --- a/homeassistant/components/wallbox/coordinator.py +++ b/homeassistant/components/wallbox/coordinator.py @@ -209,7 +209,12 @@ def _get_data(self) -> dict[str, Any]: ) from wallbox_connection_error async def _async_update_data(self) -> dict[str, Any]: - """Get new sensor data for Wallbox component.""" + """Get new sensor data for Wallbox component. Set update interval to be UPDATE_INTERVAL * #wallbox chargers configured, this is necessary due to rate limitations.""" + + self.update_interval = timedelta( + seconds=UPDATE_INTERVAL + * max(len(self.hass.config_entries.async_loaded_entries(DOMAIN)), 1) + ) return await self.hass.async_add_executor_job(self._get_data) @_require_authentication diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 8c162a7f10f7a0..dbd749370ca720 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -499,7 +499,6 @@ "playstation_network", "plex", "plugwise", - "plum_lightpad", "point", "pooldose", "poolsense", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index bd3cd7692c990e..4f49dad82dcfb0 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5087,12 +5087,6 @@ "config_flow": true, "iot_class": "local_polling" }, - "plum_lightpad": { - "name": "Plum Lightpad", - "integration_type": "hub", - "config_flow": true, - "iot_class": "local_push" - }, "pocketcasts": { "name": "Pocket Casts", "integration_type": "hub", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 34fe8d2d2bf0c2..afb46240776b43 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -60,7 +60,7 @@ pyserial==3.5 pyspeex-noise==1.0.2 python-slugify==8.0.4 PyTurboJPEG==1.8.0 -PyYAML==6.0.2 +PyYAML==6.0.3 requests==2.32.5 securetar==2025.2.1 SQLAlchemy==2.0.41 @@ -88,9 +88,9 @@ httplib2>=0.19.0 # gRPC is an implicit dependency that we want to make explicit so we manage # upgrades intentionally. It is a large package to build from source and we # want to ensure we have wheels built. -grpcio==1.72.1 -grpcio-status==1.72.1 -grpcio-reflection==1.72.1 +grpcio==1.75.1 +grpcio-status==1.75.1 +grpcio-reflection==1.75.1 # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 diff --git a/mypy.ini b/mypy.ini index 1813576cf23fc5..81776140629a2f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -5309,6 +5309,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.vivotek.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.vlc_telnet.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/pyproject.toml b/pyproject.toml index d33177e12760b6..14d15e4675f0da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ dependencies = [ "packaging>=23.1", "psutil-home-assistant==0.0.1", "python-slugify==8.0.4", - "PyYAML==6.0.2", + "PyYAML==6.0.3", "requests==2.32.5", "securetar==2025.2.1", "SQLAlchemy==2.0.41", diff --git a/requirements.txt b/requirements.txt index a1c4900b4007bf..c7e63873f9c983 100644 --- a/requirements.txt +++ b/requirements.txt @@ -37,7 +37,7 @@ orjson==3.11.3 packaging>=23.1 psutil-home-assistant==0.0.1 python-slugify==8.0.4 -PyYAML==6.0.2 +PyYAML==6.0.3 requests==2.32.5 securetar==2025.2.1 SQLAlchemy==2.0.41 diff --git a/requirements_all.txt b/requirements_all.txt index 9ceeded4b43253..46d68538f39cf8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1358,7 +1358,7 @@ letpot==0.6.2 libpyfoscamcgi==0.0.7 # homeassistant.components.vivotek -libpyvivotek==0.4.0 +libpyvivotek==0.6.1 # homeassistant.components.libre_hardware_monitor librehardwaremonitor-api==1.4.0 @@ -1722,9 +1722,6 @@ plexwebsocket==0.0.14 # homeassistant.components.plugwise plugwise==1.7.8 -# homeassistant.components.plum_lightpad -plumlightpad==0.0.11 - # homeassistant.components.serial_pm pmsensor==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b585352ff22e0a..38d599d87f3bc9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1463,9 +1463,6 @@ plexwebsocket==0.0.14 # homeassistant.components.plugwise plugwise==1.7.8 -# homeassistant.components.plum_lightpad -plumlightpad==0.0.11 - # homeassistant.components.poolsense poolsense==0.0.8 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 5d176adfdec060..bdd8ed2cda15b9 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -113,9 +113,9 @@ # gRPC is an implicit dependency that we want to make explicit so we manage # upgrades intentionally. It is a large package to build from source and we # want to ensure we have wheels built. -grpcio==1.72.1 -grpcio-status==1.72.1 -grpcio-reflection==1.72.1 +grpcio==1.75.1 +grpcio-status==1.75.1 +grpcio-reflection==1.75.1 # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 diff --git a/tests/components/blue_current/__init__.py b/tests/components/blue_current/__init__.py index 420c3bdfdc5be3..a5c4f7ff82a80f 100644 --- a/tests/components/blue_current/__init__.py +++ b/tests/components/blue_current/__init__.py @@ -120,7 +120,7 @@ async def update_charge_point( async def init_integration( hass: HomeAssistant, config_entry: MockConfigEntry, - platform="", + platform: str | None = None, charge_point: dict | None = None, status: dict | None = None, grid: dict | None = None, @@ -136,6 +136,10 @@ async def init_integration( if grid is None: grid = {} + platforms = [platform] if platform else [] + if platform: + platforms.append(platform) + future_container = FutureContainer(hass.loop.create_future()) started_loop = Event() @@ -144,7 +148,7 @@ async def init_integration( ) with ( - patch("homeassistant.components.blue_current.PLATFORMS", [platform]), + patch("homeassistant.components.blue_current.PLATFORMS", platforms), patch("homeassistant.components.blue_current.Client", return_value=client_mock), ): config_entry.add_to_hass(hass) diff --git a/tests/components/oralb/test_sensor.py b/tests/components/oralb/test_sensor.py index 147f20733d6f23..8c2c11c7dbcad2 100644 --- a/tests/components/oralb/test_sensor.py +++ b/tests/components/oralb/test_sensor.py @@ -101,7 +101,7 @@ async def test_sensors_io_series_4(hass: HomeAssistant) -> None: toothbrush_sensor = hass.states.get("sensor.io_series_4_48be_brushing_mode") toothbrush_sensor_attrs = toothbrush_sensor.attributes - assert toothbrush_sensor.state == "gum care" + assert toothbrush_sensor.state == "gum_care" assert ( toothbrush_sensor_attrs[ATTR_FRIENDLY_NAME] == "IO Series 4 48BE Brushing mode" ) @@ -133,7 +133,7 @@ async def test_sensors_io_series_4(hass: HomeAssistant) -> None: toothbrush_sensor = hass.states.get("sensor.io_series_4_48be_brushing_mode") # Sleepy devices should keep their state over time - assert toothbrush_sensor.state == "gum care" + assert toothbrush_sensor.state == "gum_care" toothbrush_sensor_attrs = toothbrush_sensor.attributes assert toothbrush_sensor_attrs[ATTR_ASSUMED_STATE] is True diff --git a/tests/components/plum_lightpad/test_config_flow.py b/tests/components/plum_lightpad/test_config_flow.py deleted file mode 100644 index ca7c110c963ec1..00000000000000 --- a/tests/components/plum_lightpad/test_config_flow.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Test the Plum Lightpad config flow.""" - -from unittest.mock import patch - -from requests.exceptions import ConnectTimeout - -from homeassistant import config_entries -from homeassistant.components.plum_lightpad.const import DOMAIN -from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultType - -from tests.common import MockConfigEntry - - -async def test_form(hass: HomeAssistant) -> None: - """Test we get the form.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] is FlowResultType.FORM - assert result["errors"] == {} - - with ( - patch("homeassistant.components.plum_lightpad.utils.Plum.loadCloudData"), - patch( - "homeassistant.components.plum_lightpad.async_setup_entry", - return_value=True, - ) as mock_setup_entry, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"username": "test-plum-username", "password": "test-plum-password"}, - ) - await hass.async_block_till_done() - - assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == "test-plum-username" - assert result2["data"] == { - "username": "test-plum-username", - "password": "test-plum-password", - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_form_cannot_connect(hass: HomeAssistant) -> None: - """Test we handle invalid auth.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.plum_lightpad.utils.Plum.loadCloudData", - side_effect=ConnectTimeout, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"username": "test-plum-username", "password": "test-plum-password"}, - ) - - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "cannot_connect"} - - -async def test_form_one_entry_per_email_allowed(hass: HomeAssistant) -> None: - """Test that only one entry allowed per Plum cloud email address.""" - MockConfigEntry( - domain=DOMAIN, - unique_id="test-plum-username", - data={"username": "test-plum-username", "password": "test-plum-password"}, - ).add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with ( - patch("homeassistant.components.plum_lightpad.utils.Plum.loadCloudData"), - patch( - "homeassistant.components.plum_lightpad.async_setup_entry" - ) as mock_setup_entry, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"username": "test-plum-username", "password": "test-plum-password"}, - ) - - assert result2["type"] is FlowResultType.ABORT - await hass.async_block_till_done() - assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/plum_lightpad/test_init.py b/tests/components/plum_lightpad/test_init.py index c34ecfd8deb314..09a140016a277d 100644 --- a/tests/components/plum_lightpad/test_init.py +++ b/tests/components/plum_lightpad/test_init.py @@ -1,91 +1,51 @@ """Tests for the Plum Lightpad config flow.""" -from unittest.mock import Mock, patch - -from aiohttp import ContentTypeError -from requests.exceptions import HTTPError - -from homeassistant.components.plum_lightpad.const import DOMAIN +from homeassistant.components.plum_lightpad import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component +from homeassistant.helpers import issue_registry as ir from tests.common import MockConfigEntry -async def test_async_setup_no_domain_config(hass: HomeAssistant) -> None: - """Test setup without configuration is noop.""" - result = await async_setup_component(hass, DOMAIN, {}) - - assert result is True - assert DOMAIN not in hass.data +async def test_repair_issue( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: + """Test Plum Lightpad repair issue.""" - -async def test_async_setup_entry_sets_up_light(hass: HomeAssistant) -> None: - """Test that configuring entry sets up light domain.""" - config_entry = MockConfigEntry( + config_entry_1 = MockConfigEntry( + title="Example 1", domain=DOMAIN, - data={"username": "test-plum-username", "password": "test-plum-password"}, ) - config_entry.add_to_hass(hass) - - with ( - patch( - "homeassistant.components.plum_lightpad.utils.Plum.loadCloudData" - ) as mock_loadCloudData, - patch( - "homeassistant.components.plum_lightpad.light.async_setup_entry" - ) as mock_light_async_setup_entry, - ): - result = await hass.config_entries.async_setup(config_entry.entry_id) - assert result is True - - await hass.async_block_till_done() - - assert len(mock_loadCloudData.mock_calls) == 1 - assert len(mock_light_async_setup_entry.mock_calls) == 1 - - -async def test_async_setup_entry_handles_auth_error(hass: HomeAssistant) -> None: - """Test that configuring entry handles Plum Cloud authentication error.""" - config_entry = MockConfigEntry( + config_entry_1.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry_1.entry_id) + await hass.async_block_till_done() + assert config_entry_1.state is ConfigEntryState.LOADED + + # Add a second one + config_entry_2 = MockConfigEntry( + title="Example 2", domain=DOMAIN, - data={"username": "test-plum-username", "password": "test-plum-password"}, ) - config_entry.add_to_hass(hass) + config_entry_2.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry_2.entry_id) + await hass.async_block_till_done() - with ( - patch( - "homeassistant.components.plum_lightpad.utils.Plum.loadCloudData", - side_effect=ContentTypeError(Mock(), None), - ), - patch( - "homeassistant.components.plum_lightpad.light.async_setup_entry" - ) as mock_light_async_setup_entry, - ): - result = await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry_2.state is ConfigEntryState.LOADED + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) - assert result is False - assert len(mock_light_async_setup_entry.mock_calls) == 0 + # Remove the first one + await hass.config_entries.async_remove(config_entry_1.entry_id) + await hass.async_block_till_done() + assert config_entry_1.state is ConfigEntryState.NOT_LOADED + assert config_entry_2.state is ConfigEntryState.LOADED + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) -async def test_async_setup_entry_handles_http_error(hass: HomeAssistant) -> None: - """Test that configuring entry handles HTTP error.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data={"username": "test-plum-username", "password": "test-plum-password"}, - ) - config_entry.add_to_hass(hass) - - with ( - patch( - "homeassistant.components.plum_lightpad.utils.Plum.loadCloudData", - side_effect=HTTPError, - ), - patch( - "homeassistant.components.plum_lightpad.light.async_setup_entry" - ) as mock_light_async_setup_entry, - ): - result = await hass.config_entries.async_setup(config_entry.entry_id) + # Remove the second one + await hass.config_entries.async_remove(config_entry_2.entry_id) + await hass.async_block_till_done() - assert result is False - assert len(mock_light_async_setup_entry.mock_calls) == 0 + assert config_entry_1.state is ConfigEntryState.NOT_LOADED + assert config_entry_2.state is ConfigEntryState.NOT_LOADED + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) is None