diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a8081884de122d..14ee68037320a1 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,11 +24,11 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Initialize CodeQL - uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5 + uses: github/codeql-action/init@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5 + uses: github/codeql-action/analyze@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6 with: category: "/language:python" diff --git a/homeassistant/brands/ibm.json b/homeassistant/brands/ibm.json deleted file mode 100644 index 42367e899e77b8..00000000000000 --- a/homeassistant/brands/ibm.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "domain": "ibm", - "name": "IBM", - "integrations": ["watson_iot", "watson_tts"] -} diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 0e1e89cf1e341d..a0cdda7b2b7dbf 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_polling", "loggers": ["pyenphase"], "quality_scale": "platinum", - "requirements": ["pyenphase==2.3.0"], + "requirements": ["pyenphase==2.4.0"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/homeassistant/components/idasen_desk/coordinator.py b/homeassistant/components/idasen_desk/coordinator.py index 5da3d57cf9a6f3..f7b7edd2cc1a7a 100644 --- a/homeassistant/components/idasen_desk/coordinator.py +++ b/homeassistant/components/idasen_desk/coordinator.py @@ -8,13 +8,16 @@ from homeassistant.components import bluetooth from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator _LOGGER = logging.getLogger(__name__) type IdasenDeskConfigEntry = ConfigEntry[IdasenDeskCoordinator] +UPDATE_DEBOUNCE_TIME = 0.2 + class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]): """Class to manage updates for the Idasen Desk.""" @@ -33,9 +36,22 @@ def __init__( hass, _LOGGER, config_entry=config_entry, name=config_entry.title ) self.address = address + self.desk = Desk(self._async_handle_update) + self._expected_connected = False + self._height: int | None = None - self.desk = Desk(self.async_set_updated_data) + @callback + def async_update_data() -> None: + self.async_set_updated_data(self._height) + + self._debouncer = Debouncer( + hass=self.hass, + logger=_LOGGER, + cooldown=UPDATE_DEBOUNCE_TIME, + immediate=True, + function=async_update_data, + ) async def async_connect(self) -> bool: """Connect to desk.""" @@ -60,3 +76,9 @@ async def async_connect_if_expected(self) -> None: """Ensure that the desk is connected if that is the expected state.""" if self._expected_connected: await self.async_connect() + + @callback + def _async_handle_update(self, height: int | None) -> None: + """Handle an update from the desk.""" + self._height = height + self._debouncer.async_schedule_call() diff --git a/homeassistant/components/portainer/__init__.py b/homeassistant/components/portainer/__init__.py index 79f7c02e4ba8be..732831b27c5e09 100644 --- a/homeassistant/components/portainer/__init__.py +++ b/homeassistant/components/portainer/__init__.py @@ -18,7 +18,7 @@ from .coordinator import PortainerCoordinator -_PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR] +_PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SWITCH] type PortainerConfigEntry = ConfigEntry[PortainerCoordinator] diff --git a/homeassistant/components/portainer/binary_sensor.py b/homeassistant/components/portainer/binary_sensor.py index 543bdeaf335d41..032b46ef8b45c7 100644 --- a/homeassistant/components/portainer/binary_sensor.py +++ b/homeassistant/components/portainer/binary_sensor.py @@ -131,15 +131,7 @@ def __init__( self.entity_description = entity_description super().__init__(device_info, coordinator, via_device) - # Container ID's are ephemeral, so use the container name for the unique ID - # The first one, should always be unique, it's fine if users have aliases - # According to Docker's API docs, the first name is unique - device_identifier = ( - self._device_info.names[0].replace("/", " ").strip() - if self._device_info.names - else None - ) - self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{device_identifier}_{entity_description.key}" + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{self.device_name}_{entity_description.key}" @property def available(self) -> bool: diff --git a/homeassistant/components/portainer/entity.py b/homeassistant/components/portainer/entity.py index 907e8cf4afe900..27355bb7c0c799 100644 --- a/homeassistant/components/portainer/entity.py +++ b/homeassistant/components/portainer/entity.py @@ -57,25 +57,25 @@ def __init__( self.device_id = self._device_info.id self.endpoint_id = via_device.endpoint.id - device_name = ( - self._device_info.names[0].replace("/", " ").strip() - if self._device_info.names - else None - ) + # Container ID's are ephemeral, so use the container name for the unique ID + # The first one, should always be unique, it's fine if users have aliases + # According to Docker's API docs, the first name is unique + assert self._device_info.names, "Container names list unexpectedly empty" + self.device_name = self._device_info.names[0].replace("/", " ").strip() self._attr_device_info = DeviceInfo( identifiers={ - (DOMAIN, f"{self.coordinator.config_entry.entry_id}_{device_name}") + (DOMAIN, f"{self.coordinator.config_entry.entry_id}_{self.device_name}") }, manufacturer=DEFAULT_NAME, configuration_url=URL( f"{coordinator.config_entry.data[CONF_URL]}#!/{self.endpoint_id}/docker/containers/{self.device_id}" ), model="Container", - name=device_name, + name=self.device_name, via_device=( DOMAIN, f"{self.coordinator.config_entry.entry_id}_{self.endpoint_id}", ), - translation_key=None if device_name else "unknown_container", + translation_key=None if self.device_name else "unknown_container", ) diff --git a/homeassistant/components/portainer/icons.json b/homeassistant/components/portainer/icons.json new file mode 100644 index 00000000000000..316851d2c67591 --- /dev/null +++ b/homeassistant/components/portainer/icons.json @@ -0,0 +1,12 @@ +{ + "entity": { + "switch": { + "container": { + "default": "mdi:arrow-down-box", + "state": { + "on": "mdi:arrow-up-box" + } + } + } + } +} diff --git a/homeassistant/components/portainer/strings.json b/homeassistant/components/portainer/strings.json index dbbfe17764f318..e48f8505277a6b 100644 --- a/homeassistant/components/portainer/strings.json +++ b/homeassistant/components/portainer/strings.json @@ -45,6 +45,11 @@ "status": { "name": "Status" } + }, + "switch": { + "container": { + "name": "Container" + } } }, "exceptions": { diff --git a/homeassistant/components/portainer/switch.py b/homeassistant/components/portainer/switch.py new file mode 100644 index 00000000000000..eed33e43c0cd0b --- /dev/null +++ b/homeassistant/components/portainer/switch.py @@ -0,0 +1,141 @@ +"""Switch platform for Portainer containers.""" + +from __future__ import annotations + +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +from typing import Any + +from pyportainer import Portainer +from pyportainer.exceptions import ( + PortainerAuthenticationError, + PortainerConnectionError, + PortainerTimeoutError, +) +from pyportainer.models.docker import DockerContainer + +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import PortainerConfigEntry +from .const import DOMAIN +from .coordinator import PortainerCoordinator +from .entity import PortainerContainerEntity, PortainerCoordinatorData + + +@dataclass(frozen=True, kw_only=True) +class PortainerSwitchEntityDescription(SwitchEntityDescription): + """Class to hold Portainer switch description.""" + + is_on_fn: Callable[[DockerContainer], bool | None] + turn_on_fn: Callable[[str, Portainer, int, str], Coroutine[Any, Any, None]] + turn_off_fn: Callable[[str, Portainer, int, str], Coroutine[Any, Any, None]] + + +async def perform_action( + action: str, portainer: Portainer, endpoint_id: int, container_id: str +) -> None: + """Stop a container.""" + try: + if action == "start": + await portainer.start_container(endpoint_id, container_id) + elif action == "stop": + await portainer.stop_container(endpoint_id, container_id) + except PortainerAuthenticationError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="invalid_auth", + translation_placeholders={"error": repr(err)}, + ) from err + except PortainerConnectionError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="cannot_connect", + translation_placeholders={"error": repr(err)}, + ) from err + except PortainerTimeoutError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="timeout_connect", + translation_placeholders={"error": repr(err)}, + ) from err + + +SWITCHES: tuple[PortainerSwitchEntityDescription, ...] = ( + PortainerSwitchEntityDescription( + key="container", + translation_key="container", + device_class=SwitchDeviceClass.SWITCH, + is_on_fn=lambda data: data.state == "running", + turn_on_fn=perform_action, + turn_off_fn=perform_action, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: PortainerConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Portainer switch sensors.""" + + coordinator = entry.runtime_data + + async_add_entities( + PortainerContainerSwitch( + coordinator=coordinator, + entity_description=entity_description, + device_info=container, + via_device=endpoint, + ) + for endpoint in coordinator.data.values() + for container in endpoint.containers.values() + for entity_description in SWITCHES + ) + + +class PortainerContainerSwitch(PortainerContainerEntity, SwitchEntity): + """Representation of a Portainer container switch.""" + + entity_description: PortainerSwitchEntityDescription + + def __init__( + self, + coordinator: PortainerCoordinator, + entity_description: PortainerSwitchEntityDescription, + device_info: DockerContainer, + via_device: PortainerCoordinatorData, + ) -> None: + """Initialize the Portainer container switch.""" + self.entity_description = entity_description + super().__init__(device_info, coordinator, via_device) + + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{self.device_name}_{entity_description.key}" + + @property + def is_on(self) -> bool | None: + """Return the state of the device.""" + return self.entity_description.is_on_fn( + self.coordinator.data[self.endpoint_id].containers[self.device_id] + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Start (turn on) the container.""" + await self.entity_description.turn_on_fn( + "start", self.coordinator.portainer, self.endpoint_id, self.device_id + ) + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Stop (turn off) the container.""" + await self.entity_description.turn_off_fn( + "stop", self.coordinator.portainer, self.endpoint_id, self.device_id + ) + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/roborock/config_flow.py b/homeassistant/components/roborock/config_flow.py index e5f449d4984768..80b90210bf3dad 100644 --- a/homeassistant/components/roborock/config_flow.py +++ b/homeassistant/components/roborock/config_flow.py @@ -129,7 +129,7 @@ async def async_step_code( reauth_entry, data_updates={CONF_USER_DATA: user_data.as_dict()} ) self._abort_if_unique_id_configured(error="already_configured_account") - return self._create_entry(self._client, self._username, user_data) + return await self._create_entry(self._client, self._username, user_data) return self.async_show_form( step_id="code", @@ -176,7 +176,7 @@ async def async_step_reauth_confirm( return await self.async_step_code() return self.async_show_form(step_id="reauth_confirm", errors=errors) - def _create_entry( + async def _create_entry( self, client: RoborockApiClient, username: str, user_data: UserData ) -> ConfigFlowResult: """Finished config flow and create entry.""" @@ -185,7 +185,7 @@ def _create_entry( data={ CONF_USERNAME: username, CONF_USER_DATA: user_data.as_dict(), - CONF_BASE_URL: client.base_url, + CONF_BASE_URL: await client.base_url, }, ) diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index e6bf46e2202af8..9339f70576b71a 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -19,7 +19,7 @@ "loggers": ["roborock"], "quality_scale": "silver", "requirements": [ - "python-roborock==2.49.1", + "python-roborock==2.50.2", "vacuum-map-parser-roborock==0.1.4" ] } diff --git a/homeassistant/components/vultr/__init__.py b/homeassistant/components/vultr/__init__.py deleted file mode 100644 index 66527bf458e7e3..00000000000000 --- a/homeassistant/components/vultr/__init__.py +++ /dev/null @@ -1,100 +0,0 @@ -"""Support for Vultr.""" - -from datetime import timedelta -import logging - -import voluptuous as vol -from vultr import Vultr as VultrAPI - -from homeassistant.components import persistent_notification -from homeassistant.const import CONF_API_KEY, Platform -from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import ConfigType -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -ATTR_AUTO_BACKUPS = "auto_backups" -ATTR_ALLOWED_BANDWIDTH = "allowed_bandwidth_gb" -ATTR_COST_PER_MONTH = "cost_per_month" -ATTR_CURRENT_BANDWIDTH_USED = "current_bandwidth_gb" -ATTR_CREATED_AT = "created_at" -ATTR_DISK = "disk" -ATTR_SUBSCRIPTION_ID = "subid" -ATTR_SUBSCRIPTION_NAME = "label" -ATTR_IPV4_ADDRESS = "ipv4_address" -ATTR_IPV6_ADDRESS = "ipv6_address" -ATTR_MEMORY = "memory" -ATTR_OS = "os" -ATTR_PENDING_CHARGES = "pending_charges" -ATTR_REGION = "region" -ATTR_VCPUS = "vcpus" - -CONF_SUBSCRIPTION = "subscription" - -DATA_VULTR = "data_vultr" -DOMAIN = "vultr" - -NOTIFICATION_ID = "vultr_notification" -NOTIFICATION_TITLE = "Vultr Setup" - -VULTR_PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) - -CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.Schema({vol.Required(CONF_API_KEY): cv.string})}, extra=vol.ALLOW_EXTRA -) - - -def setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Vultr component.""" - api_key = config[DOMAIN].get(CONF_API_KEY) - - vultr = Vultr(api_key) - - try: - vultr.update() - except RuntimeError as ex: - _LOGGER.error("Failed to make update API request because: %s", ex) - persistent_notification.create( - hass, - f"Error: {ex}", - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) - return False - - hass.data[DATA_VULTR] = vultr - return True - - -class Vultr: - """Handle all communication with the Vultr API.""" - - def __init__(self, api_key): - """Initialize the Vultr connection.""" - - self._api_key = api_key - self.data = None - self.api = VultrAPI(self._api_key) - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Use the data from Vultr API.""" - self.data = self.api.server_list() - - def _force_update(self): - """Use the data from Vultr API.""" - self.data = self.api.server_list() - - def halt(self, subscription): - """Halt a subscription (hard power off).""" - self.api.server_halt(subscription) - self._force_update() - - def start(self, subscription): - """Start a subscription.""" - self.api.server_start(subscription) - self._force_update() diff --git a/homeassistant/components/vultr/binary_sensor.py b/homeassistant/components/vultr/binary_sensor.py deleted file mode 100644 index 3972de8a625476..00000000000000 --- a/homeassistant/components/vultr/binary_sensor.py +++ /dev/null @@ -1,121 +0,0 @@ -"""Support for monitoring the state of Vultr subscriptions (VPS).""" - -from __future__ import annotations - -import logging - -import voluptuous as vol - -from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA, - BinarySensorDeviceClass, - BinarySensorEntity, -) -from homeassistant.const import CONF_NAME -from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from . import ( - ATTR_ALLOWED_BANDWIDTH, - ATTR_AUTO_BACKUPS, - ATTR_COST_PER_MONTH, - ATTR_CREATED_AT, - ATTR_DISK, - ATTR_IPV4_ADDRESS, - ATTR_IPV6_ADDRESS, - ATTR_MEMORY, - ATTR_OS, - ATTR_REGION, - ATTR_SUBSCRIPTION_ID, - ATTR_SUBSCRIPTION_NAME, - ATTR_VCPUS, - CONF_SUBSCRIPTION, - DATA_VULTR, -) - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = "Vultr {}" -PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_SUBSCRIPTION): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Vultr subscription (server) binary sensor.""" - vultr = hass.data[DATA_VULTR] - - subscription = config.get(CONF_SUBSCRIPTION) - name = config.get(CONF_NAME) - - if subscription not in vultr.data: - _LOGGER.error("Subscription %s not found", subscription) - return - - add_entities([VultrBinarySensor(vultr, subscription, name)], True) - - -class VultrBinarySensor(BinarySensorEntity): - """Representation of a Vultr subscription sensor.""" - - _attr_device_class = BinarySensorDeviceClass.POWER - - def __init__(self, vultr, subscription, name): - """Initialize a new Vultr binary sensor.""" - self._vultr = vultr - self._name = name - - self.subscription = subscription - self.data = None - - @property - def name(self): - """Return the name of the sensor.""" - try: - return self._name.format(self.data["label"]) - except (KeyError, TypeError): - return self._name - - @property - def icon(self): - """Return the icon of this server.""" - return "mdi:server" if self.is_on else "mdi:server-off" - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self.data["power_status"] == "running" - - @property - def extra_state_attributes(self): - """Return the state attributes of the Vultr subscription.""" - return { - ATTR_ALLOWED_BANDWIDTH: self.data.get("allowed_bandwidth_gb"), - ATTR_AUTO_BACKUPS: self.data.get("auto_backups"), - ATTR_COST_PER_MONTH: self.data.get("cost_per_month"), - ATTR_CREATED_AT: self.data.get("date_created"), - ATTR_DISK: self.data.get("disk"), - ATTR_IPV4_ADDRESS: self.data.get("main_ip"), - ATTR_IPV6_ADDRESS: self.data.get("v6_main_ip"), - ATTR_MEMORY: self.data.get("ram"), - ATTR_OS: self.data.get("os"), - ATTR_REGION: self.data.get("location"), - ATTR_SUBSCRIPTION_ID: self.data.get("SUBID"), - ATTR_SUBSCRIPTION_NAME: self.data.get("label"), - ATTR_VCPUS: self.data.get("vcpu_count"), - } - - def update(self) -> None: - """Update state of sensor.""" - self._vultr.update() - self.data = self._vultr.data[self.subscription] diff --git a/homeassistant/components/vultr/manifest.json b/homeassistant/components/vultr/manifest.json deleted file mode 100644 index 713485e79317fd..00000000000000 --- a/homeassistant/components/vultr/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "vultr", - "name": "Vultr", - "codeowners": [], - "documentation": "https://www.home-assistant.io/integrations/vultr", - "iot_class": "cloud_polling", - "loggers": ["vultr"], - "quality_scale": "legacy", - "requirements": ["vultr==0.1.2"] -} diff --git a/homeassistant/components/vultr/sensor.py b/homeassistant/components/vultr/sensor.py deleted file mode 100644 index c392c382cbd421..00000000000000 --- a/homeassistant/components/vultr/sensor.py +++ /dev/null @@ -1,123 +0,0 @@ -"""Support for monitoring the state of Vultr Subscriptions.""" - -from __future__ import annotations - -import logging - -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, -) -from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME, UnitOfInformation -from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from . import ( - ATTR_CURRENT_BANDWIDTH_USED, - ATTR_PENDING_CHARGES, - CONF_SUBSCRIPTION, - DATA_VULTR, -) - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = "Vultr {} {}" -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key=ATTR_CURRENT_BANDWIDTH_USED, - name="Current Bandwidth Used", - native_unit_of_measurement=UnitOfInformation.GIGABYTES, - device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:chart-histogram", - ), - SensorEntityDescription( - key=ATTR_PENDING_CHARGES, - name="Pending Charges", - native_unit_of_measurement="US$", - icon="mdi:currency-usd", - ), -) -SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] - -PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_SUBSCRIPTION): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] - ), - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Vultr subscription (server) sensor.""" - vultr = hass.data[DATA_VULTR] - - subscription = config[CONF_SUBSCRIPTION] - name = config[CONF_NAME] - monitored_conditions = config[CONF_MONITORED_CONDITIONS] - - if subscription not in vultr.data: - _LOGGER.error("Subscription %s not found", subscription) - return - - entities = [ - VultrSensor(vultr, subscription, name, description) - for description in SENSOR_TYPES - if description.key in monitored_conditions - ] - - add_entities(entities, True) - - -class VultrSensor(SensorEntity): - """Representation of a Vultr subscription sensor.""" - - def __init__( - self, vultr, subscription, name, description: SensorEntityDescription - ) -> None: - """Initialize a new Vultr sensor.""" - self.entity_description = description - self._vultr = vultr - self._name = name - - self.subscription = subscription - self.data = None - - @property - def name(self): - """Return the name of the sensor.""" - try: - return self._name.format(self.entity_description.name) - except IndexError: - try: - return self._name.format( - self.data["label"], self.entity_description.name - ) - except (KeyError, TypeError): - return self._name - - @property - def native_value(self): - """Return the value of this given sensor type.""" - try: - return round(float(self.data.get(self.entity_description.key)), 2) - except (TypeError, ValueError): - return self.data.get(self.entity_description.key) - - def update(self) -> None: - """Update state of sensor.""" - self._vultr.update() - self.data = self._vultr.data[self.subscription] diff --git a/homeassistant/components/vultr/switch.py b/homeassistant/components/vultr/switch.py deleted file mode 100644 index 0b1f2247684015..00000000000000 --- a/homeassistant/components/vultr/switch.py +++ /dev/null @@ -1,129 +0,0 @@ -"""Support for interacting with Vultr subscriptions.""" - -from __future__ import annotations - -import logging -from typing import Any - -import voluptuous as vol - -from homeassistant.components.switch import ( - PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, - SwitchEntity, -) -from homeassistant.const import CONF_NAME -from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from . import ( - ATTR_ALLOWED_BANDWIDTH, - ATTR_AUTO_BACKUPS, - ATTR_COST_PER_MONTH, - ATTR_CREATED_AT, - ATTR_DISK, - ATTR_IPV4_ADDRESS, - ATTR_IPV6_ADDRESS, - ATTR_MEMORY, - ATTR_OS, - ATTR_REGION, - ATTR_SUBSCRIPTION_ID, - ATTR_SUBSCRIPTION_NAME, - ATTR_VCPUS, - CONF_SUBSCRIPTION, - DATA_VULTR, -) - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = "Vultr {}" -PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_SUBSCRIPTION): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Vultr subscription switch.""" - vultr = hass.data[DATA_VULTR] - - subscription = config.get(CONF_SUBSCRIPTION) - name = config.get(CONF_NAME) - - if subscription not in vultr.data: - _LOGGER.error("Subscription %s not found", subscription) - return - - add_entities([VultrSwitch(vultr, subscription, name)], True) - - -class VultrSwitch(SwitchEntity): - """Representation of a Vultr subscription switch.""" - - def __init__(self, vultr, subscription, name): - """Initialize a new Vultr switch.""" - self._vultr = vultr - self._name = name - - self.subscription = subscription - self.data = None - - @property - def name(self): - """Return the name of the switch.""" - try: - return self._name.format(self.data["label"]) - except (TypeError, KeyError): - return self._name - - @property - def is_on(self): - """Return true if switch is on.""" - return self.data["power_status"] == "running" - - @property - def icon(self): - """Return the icon of this server.""" - return "mdi:server" if self.is_on else "mdi:server-off" - - @property - def extra_state_attributes(self): - """Return the state attributes of the Vultr subscription.""" - return { - ATTR_ALLOWED_BANDWIDTH: self.data.get("allowed_bandwidth_gb"), - ATTR_AUTO_BACKUPS: self.data.get("auto_backups"), - ATTR_COST_PER_MONTH: self.data.get("cost_per_month"), - ATTR_CREATED_AT: self.data.get("date_created"), - ATTR_DISK: self.data.get("disk"), - ATTR_IPV4_ADDRESS: self.data.get("main_ip"), - ATTR_IPV6_ADDRESS: self.data.get("v6_main_ip"), - ATTR_MEMORY: self.data.get("ram"), - ATTR_OS: self.data.get("os"), - ATTR_REGION: self.data.get("location"), - ATTR_SUBSCRIPTION_ID: self.data.get("SUBID"), - ATTR_SUBSCRIPTION_NAME: self.data.get("label"), - ATTR_VCPUS: self.data.get("vcpu_count"), - } - - def turn_on(self, **kwargs: Any) -> None: - """Boot-up the subscription.""" - if self.data["power_status"] != "running": - self._vultr.start(self.subscription) - - def turn_off(self, **kwargs: Any) -> None: - """Halt the subscription.""" - if self.data["power_status"] == "running": - self._vultr.halt(self.subscription) - - def update(self) -> None: - """Get the latest data from the device and update the data.""" - self._vultr.update() - self.data = self._vultr.data[self.subscription] diff --git a/homeassistant/components/watson_iot/__init__.py b/homeassistant/components/watson_iot/__init__.py deleted file mode 100644 index 0130b53930ba6d..00000000000000 --- a/homeassistant/components/watson_iot/__init__.py +++ /dev/null @@ -1,227 +0,0 @@ -"""Support for the IBM Watson IoT Platform.""" - -import logging -import queue -import threading -import time - -from ibmiotf import MissingMessageEncoderException -from ibmiotf.gateway import Client -import voluptuous as vol - -from homeassistant.const import ( - CONF_DOMAINS, - CONF_ENTITIES, - CONF_EXCLUDE, - CONF_ID, - CONF_INCLUDE, - CONF_TOKEN, - CONF_TYPE, - EVENT_HOMEASSISTANT_STOP, - EVENT_STATE_CHANGED, - STATE_UNAVAILABLE, - STATE_UNKNOWN, -) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv, state as state_helper -from homeassistant.helpers.typing import ConfigType - -_LOGGER = logging.getLogger(__name__) - -CONF_ORG = "organization" - -DOMAIN = "watson_iot" - -MAX_TRIES = 3 - -RETRY_DELAY = 20 - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - vol.Schema( - { - vol.Required(CONF_ORG): cv.string, - vol.Required(CONF_TYPE): cv.string, - vol.Required(CONF_ID): cv.string, - vol.Required(CONF_TOKEN): cv.string, - vol.Optional(CONF_EXCLUDE, default={}): vol.Schema( - { - vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids, - vol.Optional(CONF_DOMAINS, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - } - ), - vol.Optional(CONF_INCLUDE, default={}): vol.Schema( - { - vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids, - vol.Optional(CONF_DOMAINS, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - } - ), - } - ) - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -def setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Watson IoT Platform component.""" - - conf = config[DOMAIN] - - include = conf[CONF_INCLUDE] - exclude = conf[CONF_EXCLUDE] - include_e = set(include[CONF_ENTITIES]) - include_d = set(include[CONF_DOMAINS]) - exclude_e = set(exclude[CONF_ENTITIES]) - exclude_d = set(exclude[CONF_DOMAINS]) - - client_args = { - "org": conf[CONF_ORG], - "type": conf[CONF_TYPE], - "id": conf[CONF_ID], - "auth-method": "token", - "auth-token": conf[CONF_TOKEN], - } - watson_gateway = Client(client_args) - - def event_to_json(event): - """Add an event to the outgoing list.""" - state = event.data.get("new_state") - if ( - state is None - or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE) - or state.entity_id in exclude_e - or state.domain in exclude_d - ): - return None - - if (include_e and state.entity_id not in include_e) or ( - include_d and state.domain not in include_d - ): - return None - - try: - _state_as_value = float(state.state) - except ValueError: - _state_as_value = None - - if _state_as_value is None: - try: - _state_as_value = float(state_helper.state_as_number(state)) - except ValueError: - _state_as_value = None - - out_event = { - "tags": {"domain": state.domain, "entity_id": state.object_id}, - "time": event.time_fired.isoformat(), - "fields": {"state": state.state}, - } - if _state_as_value is not None: - out_event["fields"]["state_value"] = _state_as_value - - for key, value in state.attributes.items(): - if key != "unit_of_measurement": - # If the key is already in fields - if key in out_event["fields"]: - key = f"{key}_" - # For each value we try to cast it as float - # But if we cannot do it we store the value - # as string - try: - out_event["fields"][key] = float(value) - except (ValueError, TypeError): - out_event["fields"][key] = str(value) - - return out_event - - instance = hass.data[DOMAIN] = WatsonIOTThread(hass, watson_gateway, event_to_json) - instance.start() - - def shutdown(event): - """Shut down the thread.""" - instance.queue.put(None) - instance.join() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) - - return True - - -class WatsonIOTThread(threading.Thread): - """A threaded event handler class.""" - - def __init__(self, hass, gateway, event_to_json): - """Initialize the listener.""" - threading.Thread.__init__(self, name="WatsonIOT") - self.queue = queue.Queue() - self.gateway = gateway - self.gateway.connect() - self.event_to_json = event_to_json - self.write_errors = 0 - self.shutdown = False - hass.bus.listen(EVENT_STATE_CHANGED, self._event_listener) - - @callback - def _event_listener(self, event): - """Listen for new messages on the bus and queue them for Watson IoT.""" - item = (time.monotonic(), event) - self.queue.put(item) - - def get_events_json(self): - """Return an event formatted for writing.""" - events = [] - - try: - if (item := self.queue.get()) is None: - self.shutdown = True - else: - event_json = self.event_to_json(item[1]) - if event_json: - events.append(event_json) - - except queue.Empty: - pass - - return events - - def write_to_watson(self, events): - """Write preprocessed events to watson.""" - - for event in events: - for retry in range(MAX_TRIES + 1): - try: - for field in event["fields"]: - value = event["fields"][field] - device_success = self.gateway.publishDeviceEvent( - event["tags"]["domain"], - event["tags"]["entity_id"], - field, - "json", - value, - ) - if not device_success: - _LOGGER.error("Failed to publish message to Watson IoT") - continue - break - except (MissingMessageEncoderException, OSError): - if retry < MAX_TRIES: - time.sleep(RETRY_DELAY) - else: - _LOGGER.exception("Failed to publish message to Watson IoT") - - def run(self): - """Process incoming events.""" - while not self.shutdown: - if event := self.get_events_json(): - self.write_to_watson(event) - self.queue.task_done() - - def block_till_done(self): - """Block till all events processed.""" - self.queue.join() diff --git a/homeassistant/components/watson_iot/manifest.json b/homeassistant/components/watson_iot/manifest.json deleted file mode 100644 index a457dcc44b1beb..00000000000000 --- a/homeassistant/components/watson_iot/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "watson_iot", - "name": "IBM Watson IoT Platform", - "codeowners": [], - "documentation": "https://www.home-assistant.io/integrations/watson_iot", - "iot_class": "cloud_push", - "loggers": ["ibmiotf", "paho_mqtt"], - "quality_scale": "legacy", - "requirements": ["ibmiotf==0.3.4"] -} diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 1909384639d3cc..f1f820fa734f63 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -703,7 +703,15 @@ async def async_step_rf_region( async def async_step_on_supervisor( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: - """Handle logic when on Supervisor host.""" + """Handle logic when on Supervisor host. + + When the add-on is running, we copy over it's settings. + We will ignore settings for USB/Socket if those were discovered. + + If add-on is not running, we will configure the add-on. + + When it's not installed, we install it with new config options. + """ if user_input is None: return self.async_show_form( step_id="on_supervisor", data_schema=ON_SUPERVISOR_SCHEMA @@ -717,8 +725,11 @@ async def async_step_on_supervisor( if addon_info.state == AddonState.RUNNING: addon_config = addon_info.options - self.usb_path = addon_config.get(CONF_ADDON_DEVICE) - self.socket_path = addon_config.get(CONF_ADDON_SOCKET) + # Use the options set by USB/ESPHome discovery + if not self._adapter_discovered: + self.usb_path = addon_config.get(CONF_ADDON_DEVICE) + self.socket_path = addon_config.get(CONF_ADDON_SOCKET) + self.s0_legacy_key = addon_config.get(CONF_ADDON_S0_LEGACY_KEY, "") self.s2_access_control_key = addon_config.get( CONF_ADDON_S2_ACCESS_CONTROL_KEY, "" @@ -931,6 +942,21 @@ async def async_step_finish_addon_setup_user( str(self.version_info.home_id), raise_on_progress=False ) + # When we came from discovery, make sure we update the add-on + if self._adapter_discovered and self.use_addon: + await self._async_set_addon_config( + { + CONF_ADDON_DEVICE: self.usb_path, + CONF_ADDON_SOCKET: self.socket_path, + CONF_ADDON_S0_LEGACY_KEY: self.s0_legacy_key, + CONF_ADDON_S2_ACCESS_CONTROL_KEY: self.s2_access_control_key, + CONF_ADDON_S2_AUTHENTICATED_KEY: self.s2_authenticated_key, + CONF_ADDON_S2_UNAUTHENTICATED_KEY: self.s2_unauthenticated_key, + CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY: self.lr_s2_access_control_key, + CONF_ADDON_LR_S2_AUTHENTICATED_KEY: self.lr_s2_authenticated_key, + } + ) + self._abort_if_unique_id_configured( updates={ CONF_URL: self.ws_address, diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 08f08b24d59771..bd3cd7692c990e 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2923,23 +2923,6 @@ "iot_class": "cloud_polling", "single_config_entry": true }, - "ibm": { - "name": "IBM", - "integrations": { - "watson_iot": { - "integration_type": "hub", - "config_flow": false, - "iot_class": "cloud_push", - "name": "IBM Watson IoT Platform" - }, - "watson_tts": { - "integration_type": "hub", - "config_flow": false, - "iot_class": "cloud_push", - "name": "IBM Watson TTS" - } - } - }, "idteck_prox": { "name": "IDTECK Proximity Reader", "integration_type": "hub", @@ -7411,12 +7394,6 @@ "config_flow": true, "iot_class": "cloud_polling" }, - "vultr": { - "name": "Vultr", - "integration_type": "hub", - "config_flow": false, - "iot_class": "cloud_polling" - }, "w800rf32": { "name": "WGL Designs W800RF32", "integration_type": "hub", @@ -7453,6 +7430,12 @@ "config_flow": true, "iot_class": "local_push" }, + "watson_tts": { + "name": "IBM Watson TTS", + "integration_type": "hub", + "config_flow": false, + "iot_class": "cloud_push" + }, "watttime": { "name": "WattTime", "integration_type": "service", diff --git a/requirements_all.txt b/requirements_all.txt index cefc93148c875e..7c69d19303abdd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1218,9 +1218,6 @@ iaqualink==0.6.0 # homeassistant.components.ibeacon ibeacon-ble==1.2.0 -# homeassistant.components.watson_iot -ibmiotf==0.3.4 - # homeassistant.components.google # homeassistant.components.local_calendar # homeassistant.components.local_todo @@ -2000,7 +1997,7 @@ pyegps==0.2.5 pyemoncms==0.1.3 # homeassistant.components.enphase_envoy -pyenphase==2.3.0 +pyenphase==2.4.0 # homeassistant.components.envisalink pyenvisalink==4.7 @@ -2550,7 +2547,7 @@ python-rabbitair==0.0.8 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==2.49.1 +python-roborock==2.50.2 # homeassistant.components.smarttub python-smarttub==0.0.44 @@ -3118,9 +3115,6 @@ vsure==2.6.7 # homeassistant.components.vasttrafik vtjp==0.2.1 -# homeassistant.components.vultr -vultr==0.1.2 - # homeassistant.components.samsungtv # homeassistant.components.wake_on_lan wakeonlan==3.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 431a13462d0e9b..579bc064ee96cb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1675,7 +1675,7 @@ pyegps==0.2.5 pyemoncms==0.1.3 # homeassistant.components.enphase_envoy -pyenphase==2.3.0 +pyenphase==2.4.0 # homeassistant.components.everlights pyeverlights==0.1.0 @@ -2120,7 +2120,7 @@ python-pooldose==0.5.0 python-rabbitair==0.0.8 # homeassistant.components.roborock -python-roborock==2.49.1 +python-roborock==2.50.2 # homeassistant.components.smarttub python-smarttub==0.0.44 @@ -2583,9 +2583,6 @@ volvocarsapi==0.4.2 # homeassistant.components.verisure vsure==2.6.7 -# homeassistant.components.vultr -vultr==0.1.2 - # homeassistant.components.samsungtv # homeassistant.components.wake_on_lan wakeonlan==3.1.0 diff --git a/script/hassfest/quality_scale.py b/script/hassfest/quality_scale.py index 01cca31a90dfea..7468afab89045d 100644 --- a/script/hassfest/quality_scale.py +++ b/script/hassfest/quality_scale.py @@ -1062,13 +1062,11 @@ class Rule: "volkszaehler", "volumio", "volvooncall", - "vultr", "w800rf32", "wake_on_lan", "wallbox", "waqi", "waterfurnace", - "watson_iot", "watson_tts", "watttime", "waze_travel_time", @@ -2112,13 +2110,11 @@ class Rule: "volkszaehler", "volumio", "volvooncall", - "vultr", "w800rf32", "wake_on_lan", "wallbox", "waqi", "waterfurnace", - "watson_iot", "watson_tts", "watttime", "waze_travel_time", diff --git a/tests/components/habitica/__snapshots__/test_image/test_image_platform.1.png b/tests/components/habitica/__snapshots__/test_image/test_image_platform.1.png deleted file mode 100644 index 5bb8c9d9f091c7..00000000000000 Binary files a/tests/components/habitica/__snapshots__/test_image/test_image_platform.1.png and /dev/null differ diff --git a/tests/components/habitica/__snapshots__/test_image/test_image_platform.png b/tests/components/habitica/__snapshots__/test_image/test_image_platform.png deleted file mode 100644 index 8e9b046ee05dbf..00000000000000 Binary files a/tests/components/habitica/__snapshots__/test_image/test_image_platform.png and /dev/null differ diff --git a/tests/components/habitica/test_config_flow.py b/tests/components/habitica/test_config_flow.py index 63001157695c1b..a393c7a60824a9 100644 --- a/tests/components/habitica/test_config_flow.py +++ b/tests/components/habitica/test_config_flow.py @@ -87,7 +87,6 @@ async def test_form_login(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> N result["flow_id"], user_input=MOCK_DATA_LOGIN_STEP, ) - await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test-user" @@ -208,7 +207,6 @@ async def test_form_advanced(hass: HomeAssistant, mock_setup_entry: AsyncMock) - result["flow_id"], user_input=MOCK_DATA_ADVANCED_STEP, ) - await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test-user" @@ -329,8 +327,6 @@ async def test_flow_reauth( user_input, ) - await hass.async_block_till_done() - assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert config_entry.data[CONF_API_KEY] == "cd0e5985-17de-4b4f-849e-5d506c5e4382" @@ -399,8 +395,6 @@ async def test_flow_reauth_errors( result["flow_id"], user_input ) - await hass.async_block_till_done() - assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": text_error} @@ -412,8 +406,6 @@ async def test_flow_reauth_errors( user_input=USER_INPUT_REAUTH_API_KEY, ) - await hass.async_block_till_done() - assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert config_entry.data[CONF_API_KEY] == "cd0e5985-17de-4b4f-849e-5d506c5e4382" @@ -446,8 +438,6 @@ async def test_flow_reauth_unique_id_mismatch(hass: HomeAssistant) -> None: USER_INPUT_REAUTH_LOGIN, ) - await hass.async_block_till_done() - assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unique_id_mismatch" @@ -469,8 +459,6 @@ async def test_flow_reconfigure( USER_INPUT_RECONFIGURE, ) - await hass.async_block_till_done() - assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" assert config_entry.data[CONF_API_KEY] == "cd0e5985-17de-4b4f-849e-5d506c5e4382" @@ -507,8 +495,6 @@ async def test_flow_reconfigure_errors( USER_INPUT_RECONFIGURE, ) - await hass.async_block_till_done() - assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": text_error} @@ -519,8 +505,6 @@ async def test_flow_reconfigure_errors( user_input=USER_INPUT_RECONFIGURE, ) - await hass.async_block_till_done() - assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" assert config_entry.data[CONF_API_KEY] == "cd0e5985-17de-4b4f-849e-5d506c5e4382" diff --git a/tests/components/habitica/test_image.py b/tests/components/habitica/test_image.py index b0810d8e76f89e..d174b016e64b22 100644 --- a/tests/components/habitica/test_image.py +++ b/tests/components/habitica/test_image.py @@ -12,7 +12,6 @@ import pytest import respx from syrupy.assertion import SnapshotAssertion -from syrupy.extensions.image import PNGImageSnapshotExtension from homeassistant.components.habitica.const import ASSETS_URL, DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -50,12 +49,8 @@ async def test_image_platform( "homeassistant.components.habitica.coordinator.BytesIO", ) as avatar: avatar.side_effect = [ - BytesIO( - b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\rIDATx\xdac\xfc\xcf\xc0\xf0\x1f\x00\x05\x05\x02\x00_\xc8\xf1\xd2\x00\x00\x00\x00IEND\xaeB`\x82" - ), - BytesIO( - b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\rIDATx\xdacd`\xf8\xff\x1f\x00\x03\x07\x02\x000&\xc7a\x00\x00\x00\x00IEND\xaeB`\x82" - ), + BytesIO(b"\x89PNGTestImage1"), + BytesIO(b"\x89PNGTestImage2"), ] config_entry.add_to_hass(hass) @@ -77,9 +72,7 @@ async def test_image_platform( resp = await client.get(state.attributes["entity_picture"]) assert resp.status == HTTPStatus.OK - assert (await resp.read()) == snapshot( - extension_class=PNGImageSnapshotExtension - ) + assert (await resp.read()) == b"\x89PNGTestImage1" habitica.get_user.return_value = HabiticaUserResponse.from_json( await async_load_fixture(hass, "rogue_fixture.json", DOMAIN) @@ -95,9 +88,7 @@ async def test_image_platform( resp = await client.get(state.attributes["entity_picture"]) assert resp.status == HTTPStatus.OK - assert (await resp.read()) == snapshot( - extension_class=PNGImageSnapshotExtension - ) + assert (await resp.read()) == b"\x89PNGTestImage2" @pytest.mark.usefixtures("habitica") diff --git a/tests/components/idasen_desk/__init__.py b/tests/components/idasen_desk/__init__.py index b0d7cc5ac05b99..42e00157b8f2bf 100644 --- a/tests/components/idasen_desk/__init__.py +++ b/tests/components/idasen_desk/__init__.py @@ -38,6 +38,8 @@ tx_power=-127, ) +UPDATE_DEBOUNCE_TIME = 0.2 + async def init_integration(hass: HomeAssistant) -> MockConfigEntry: """Set up the IKEA Idasen Desk integration in Home Assistant.""" diff --git a/tests/components/idasen_desk/test_cover.py b/tests/components/idasen_desk/test_cover.py index 83312c04e725c4..84861ab6873650 100644 --- a/tests/components/idasen_desk/test_cover.py +++ b/tests/components/idasen_desk/test_cover.py @@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, MagicMock from bleak.exc import BleakError +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.cover import ( @@ -22,12 +23,13 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from . import init_integration +from . import UPDATE_DEBOUNCE_TIME, init_integration + +from tests.common import async_fire_time_changed async def test_cover_available( - hass: HomeAssistant, - mock_desk_api: MagicMock, + hass: HomeAssistant, mock_desk_api: MagicMock, freezer: FrozenDateTimeFactory ) -> None: """Test cover available property.""" entity_id = "cover.test" @@ -42,6 +44,9 @@ async def test_cover_available( mock_desk_api.is_connected = False mock_desk_api.trigger_update_callback(None) + freezer.tick(UPDATE_DEBOUNCE_TIME) + async_fire_time_changed(hass) + state = hass.states.get(entity_id) assert state assert state.state == STATE_UNAVAILABLE @@ -64,6 +69,7 @@ async def test_cover_services( service_data: dict[str, Any], expected_state: str, expected_position: int, + freezer: FrozenDateTimeFactory, ) -> None: """Test cover services.""" entity_id = "cover.test" @@ -78,7 +84,9 @@ async def test_cover_services( {"entity_id": entity_id, **service_data}, blocking=True, ) - await hass.async_block_till_done() + freezer.tick(UPDATE_DEBOUNCE_TIME) + async_fire_time_changed(hass) + state = hass.states.get(entity_id) assert state assert state.state == expected_state @@ -113,4 +121,3 @@ async def test_cover_services_exception( {"entity_id": entity_id, **service_data}, blocking=True, ) - await hass.async_block_till_done() diff --git a/tests/components/idasen_desk/test_sensor.py b/tests/components/idasen_desk/test_sensor.py index 614bce523e6ddb..dc8d6f4adf8d66 100644 --- a/tests/components/idasen_desk/test_sensor.py +++ b/tests/components/idasen_desk/test_sensor.py @@ -2,18 +2,23 @@ from unittest.mock import MagicMock +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant -from . import init_integration +from . import UPDATE_DEBOUNCE_TIME, init_integration + +from tests.common import async_fire_time_changed EXPECTED_INITIAL_HEIGHT = "1" @pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_height_sensor(hass: HomeAssistant, mock_desk_api: MagicMock) -> None: +async def test_height_sensor( + hass: HomeAssistant, mock_desk_api: MagicMock, freezer: FrozenDateTimeFactory +) -> None: """Test height sensor.""" await init_integration(hass) @@ -24,6 +29,15 @@ async def test_height_sensor(hass: HomeAssistant, mock_desk_api: MagicMock) -> N mock_desk_api.height = 1.2 mock_desk_api.trigger_update_callback(None) + await hass.async_block_till_done() + + # State should still be the same due to the debouncer + state = hass.states.get(entity_id) + assert state + assert state.state == EXPECTED_INITIAL_HEIGHT + + freezer.tick(UPDATE_DEBOUNCE_TIME) + async_fire_time_changed(hass) state = hass.states.get(entity_id) assert state @@ -34,6 +48,7 @@ async def test_height_sensor(hass: HomeAssistant, mock_desk_api: MagicMock) -> N async def test_sensor_available( hass: HomeAssistant, mock_desk_api: MagicMock, + freezer: FrozenDateTimeFactory, ) -> None: """Test sensor available property.""" await init_integration(hass) @@ -46,6 +61,9 @@ async def test_sensor_available( mock_desk_api.is_connected = False mock_desk_api.trigger_update_callback(None) + freezer.tick(UPDATE_DEBOUNCE_TIME) + async_fire_time_changed(hass) + state = hass.states.get(entity_id) assert state assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/portainer/conftest.py b/tests/components/portainer/conftest.py index 21298da10484f1..90a3fe65b15847 100644 --- a/tests/components/portainer/conftest.py +++ b/tests/components/portainer/conftest.py @@ -49,6 +49,8 @@ def mock_portainer_client() -> Generator[AsyncMock]: DockerContainer.from_dict(container) for container in load_json_array_fixture("containers.json", DOMAIN) ] + client.start_container = AsyncMock(return_value=None) + client.stop_container = AsyncMock(return_value=None) yield client diff --git a/tests/components/portainer/snapshots/test_switch.ambr b/tests/components/portainer/snapshots/test_switch.ambr new file mode 100644 index 00000000000000..6e749d8212f3d5 --- /dev/null +++ b/tests/components/portainer/snapshots/test_switch.ambr @@ -0,0 +1,246 @@ +# serializer version: 1 +# name: test_all_switch_entities_snapshot[switch.focused_einstein_container-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.focused_einstein_container', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Container', + 'platform': 'portainer', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'container', + 'unique_id': 'portainer_test_entry_123_focused_einstein_container', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_switch_entities_snapshot[switch.focused_einstein_container-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'focused_einstein Container', + }), + 'context': , + 'entity_id': 'switch.focused_einstein_container', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_all_switch_entities_snapshot[switch.funny_chatelet_container-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.funny_chatelet_container', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Container', + 'platform': 'portainer', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'container', + 'unique_id': 'portainer_test_entry_123_funny_chatelet_container', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_switch_entities_snapshot[switch.funny_chatelet_container-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'funny_chatelet Container', + }), + 'context': , + 'entity_id': 'switch.funny_chatelet_container', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_all_switch_entities_snapshot[switch.practical_morse_container-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.practical_morse_container', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Container', + 'platform': 'portainer', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'container', + 'unique_id': 'portainer_test_entry_123_practical_morse_container', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_switch_entities_snapshot[switch.practical_morse_container-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'practical_morse Container', + }), + 'context': , + 'entity_id': 'switch.practical_morse_container', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_all_switch_entities_snapshot[switch.serene_banach_container-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.serene_banach_container', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Container', + 'platform': 'portainer', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'container', + 'unique_id': 'portainer_test_entry_123_serene_banach_container', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_switch_entities_snapshot[switch.serene_banach_container-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'serene_banach Container', + }), + 'context': , + 'entity_id': 'switch.serene_banach_container', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_all_switch_entities_snapshot[switch.stoic_turing_container-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.stoic_turing_container', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Container', + 'platform': 'portainer', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'container', + 'unique_id': 'portainer_test_entry_123_stoic_turing_container', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_switch_entities_snapshot[switch.stoic_turing_container-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'stoic_turing Container', + }), + 'context': , + 'entity_id': 'switch.stoic_turing_container', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/portainer/test_switch.py b/tests/components/portainer/test_switch.py new file mode 100644 index 00000000000000..c738c1a264ff85 --- /dev/null +++ b/tests/components/portainer/test_switch.py @@ -0,0 +1,126 @@ +"""Tests for the Portainer switch platform.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock, patch + +from pyportainer.exceptions import ( + PortainerAuthenticationError, + PortainerConnectionError, + PortainerTimeoutError, +) +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.usefixtures("mock_portainer_client") +async def test_all_switch_entities_snapshot( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Snapshot test for all Portainer switch entities.""" + with patch( + "homeassistant.components.portainer._PLATFORMS", + [Platform.SWITCH], + ): + await setup_integration(hass, mock_config_entry) + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry.entry_id + ) + + +@pytest.mark.parametrize( + ("service_call", "client_method"), + [ + (SERVICE_TURN_ON, "start_container"), + (SERVICE_TURN_OFF, "stop_container"), + ], +) +async def test_turn_off_on( + hass: HomeAssistant, + mock_portainer_client: AsyncMock, + mock_config_entry: MockConfigEntry, + service_call: str, + client_method: str, +) -> None: + """Test the switches. Have you tried to turn it off and on again?""" + with patch( + "homeassistant.components.portainer._PLATFORMS", + [Platform.SWITCH], + ): + await setup_integration(hass, mock_config_entry) + + entity_id = "switch.practical_morse_container" + method_mock = getattr(mock_portainer_client, client_method) + + await hass.services.async_call( + SWITCH_DOMAIN, + service_call, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + # Matches the endpoint ID and container ID + method_mock.assert_called_once_with( + 1, "ee20facfb3b3ed4cd362c1e88fc89a53908ad05fb3a4103bca3f9b28292d14bf" + ) + + +@pytest.mark.parametrize( + ("service_call", "client_method"), + [ + (SERVICE_TURN_ON, "start_container"), + (SERVICE_TURN_OFF, "stop_container"), + ], +) +@pytest.mark.parametrize( + ("raise_exception", "expected_exception"), + [ + (PortainerAuthenticationError, HomeAssistantError), + (PortainerConnectionError, HomeAssistantError), + (PortainerTimeoutError, HomeAssistantError), + ], +) +async def test_turn_off_on_exceptions( + hass: HomeAssistant, + mock_portainer_client: AsyncMock, + mock_config_entry: MockConfigEntry, + service_call: str, + client_method: str, + raise_exception: Exception, + expected_exception: Exception, +) -> None: + """Test the switches. Have you tried to turn it off and on again? This time they will do boom!""" + with patch( + "homeassistant.components.portainer._PLATFORMS", + [Platform.SWITCH], + ): + await setup_integration(hass, mock_config_entry) + + entity_id = "switch.practical_morse_container" + method_mock = getattr(mock_portainer_client, client_method) + + method_mock.side_effect = raise_exception + with pytest.raises(expected_exception): + await hass.services.async_call( + SWITCH_DOMAIN, + service_call, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index e4731c6e9f23ad..ea569399ace7a4 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -1,11 +1,12 @@ """Global fixtures for Roborock integration.""" +import asyncio from collections.abc import Generator from copy import deepcopy import pathlib import tempfile from typing import Any -from unittest.mock import Mock, patch +from unittest.mock import Mock, PropertyMock, patch import pytest from roborock import RoborockCategory, RoomMapping @@ -70,6 +71,9 @@ async def update_values( @pytest.fixture(name="bypass_api_client_fixture") def bypass_api_client_fixture() -> None: """Skip calls to the API client.""" + base_url_future = asyncio.Future() + base_url_future.set_result(BASE_URL) + with ( patch( "homeassistant.components.roborock.RoborockApiClient.get_home_data_v3", @@ -82,6 +86,11 @@ def bypass_api_client_fixture() -> None: patch( "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.load_multi_map" ), + patch( + "homeassistant.components.roborock.config_flow.RoborockApiClient.base_url", + new_callable=PropertyMock, + return_value=base_url_future, + ), ): yield diff --git a/tests/components/roborock/mock_data.py b/tests/components/roborock/mock_data.py index cf4f167ef7fb42..1495dcb686c674 100644 --- a/tests/components/roborock/mock_data.py +++ b/tests/components/roborock/mock_data.py @@ -61,7 +61,7 @@ MOCK_CONFIG = { CONF_USERNAME: USER_EMAIL, CONF_USER_DATA: USER_DATA.as_dict(), - CONF_BASE_URL: None, + CONF_BASE_URL: BASE_URL, } HOME_DATA_RAW = { diff --git a/tests/components/roborock/snapshots/test_diagnostics.ambr b/tests/components/roborock/snapshots/test_diagnostics.ambr index ed1c37f6fa2d0b..bf7fbfaadc399f 100644 --- a/tests/components/roborock/snapshots/test_diagnostics.ambr +++ b/tests/components/roborock/snapshots/test_diagnostics.ambr @@ -216,9 +216,9 @@ 'squareMeterCleanArea': 1159.2, }), 'consumable': dict({ - 'cleaningBrushTimeLeft': 1079935, + 'cleaningBrushTimeLeft': 235, 'cleaningBrushWorkTimes': 65, - 'dustCollectionTimeLeft': 80975, + 'dustCollectionTimeLeft': 65, 'dustCollectionWorkTimes': 25, 'filterElementWorkTime': 0, 'filterTimeLeft': 465618, @@ -229,7 +229,7 @@ 'sensorTimeLeft': 33618, 'sideBrushTimeLeft': 645618, 'sideBrushWorkTime': 74382, - 'strainerTimeLeft': 539935, + 'strainerTimeLeft': 85, 'strainerWorkTimes': 65, }), 'lastCleanRecord': dict({ @@ -501,9 +501,9 @@ 'squareMeterCleanArea': 1159.2, }), 'consumable': dict({ - 'cleaningBrushTimeLeft': 1079935, + 'cleaningBrushTimeLeft': 235, 'cleaningBrushWorkTimes': 65, - 'dustCollectionTimeLeft': 80975, + 'dustCollectionTimeLeft': 65, 'dustCollectionWorkTimes': 25, 'filterElementWorkTime': 0, 'filterTimeLeft': 465618, @@ -514,7 +514,7 @@ 'sensorTimeLeft': 33618, 'sideBrushTimeLeft': 645618, 'sideBrushWorkTime': 74382, - 'strainerTimeLeft': 539935, + 'strainerTimeLeft': 85, 'strainerWorkTimes': 65, }), 'lastCleanRecord': dict({ diff --git a/tests/components/vultr/__init__.py b/tests/components/vultr/__init__.py deleted file mode 100644 index fb25b7e145e76a..00000000000000 --- a/tests/components/vultr/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the vultr component.""" diff --git a/tests/components/vultr/conftest.py b/tests/components/vultr/conftest.py deleted file mode 100644 index ae0ce9d68864a1..00000000000000 --- a/tests/components/vultr/conftest.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Test configuration for the Vultr tests.""" - -import json -from unittest.mock import patch - -import pytest -from requests_mock import Mocker - -from homeassistant.components import vultr -from homeassistant.core import HomeAssistant - -from .const import VALID_CONFIG - -from tests.common import load_fixture - - -@pytest.fixture(name="valid_config") -def valid_config(hass: HomeAssistant, requests_mock: Mocker) -> None: - """Load a valid config.""" - requests_mock.get( - "https://api.vultr.com/v1/account/info?api_key=ABCDEFG1234567", - text=load_fixture("account_info.json", "vultr"), - ) - - with patch( - "vultr.Vultr.server_list", - return_value=json.loads(load_fixture("server_list.json", "vultr")), - ): - # Setup hub - vultr.setup(hass, VALID_CONFIG) diff --git a/tests/components/vultr/const.py b/tests/components/vultr/const.py deleted file mode 100644 index 06bbf2a74835e7..00000000000000 --- a/tests/components/vultr/const.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Constants for the Vultr tests.""" - -VALID_CONFIG = {"vultr": {"api_key": "ABCDEFG1234567"}} diff --git a/tests/components/vultr/fixtures/account_info.json b/tests/components/vultr/fixtures/account_info.json deleted file mode 100644 index 89845dff4cecbc..00000000000000 --- a/tests/components/vultr/fixtures/account_info.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "balance": "-123.00", - "pending_charges": "3.38", - "last_payment_date": "2017-08-11 15:04:04", - "last_payment_amount": "-10.00" -} diff --git a/tests/components/vultr/fixtures/server_list.json b/tests/components/vultr/fixtures/server_list.json deleted file mode 100644 index 259f2931e7f74d..00000000000000 --- a/tests/components/vultr/fixtures/server_list.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "576965": { - "SUBID": "576965", - "os": "CentOS 6 x64", - "ram": "4096 MB", - "disk": "Virtual 60 GB", - "main_ip": "123.123.123.123", - "vcpu_count": "2", - "location": "New Jersey", - "DCID": "1", - "default_password": "nreqnusibni", - "date_created": "2013-12-19 14:45:41", - "pending_charges": "46.67", - "status": "active", - "cost_per_month": "10.05", - "current_bandwidth_gb": 131.512, - "allowed_bandwidth_gb": "1000", - "netmask_v4": "255.255.255.248", - "gateway_v4": "123.123.123.1", - "power_status": "running", - "server_state": "ok", - "VPSPLANID": "28", - "v6_network": "2001:DB8:1000::", - "v6_main_ip": "2001:DB8:1000::100", - "v6_network_size": "64", - "v6_networks": [ - { - "v6_network": "2001:DB8:1000::", - "v6_main_ip": "2001:DB8:1000::100", - "v6_network_size": "64" - } - ], - "label": "my new server", - "internal_ip": "10.99.0.10", - "kvm_url": "https://my.vultr.com/subs/novnc/api.php?data=eawxFVZw2mXnhGUV", - "auto_backups": "yes", - "tag": "mytag", - "OSID": "127", - "APPID": "0", - "FIREWALLGROUPID": "0" - }, - "123456": { - "SUBID": "123456", - "os": "CentOS 6 x64", - "ram": "4096 MB", - "disk": "Virtual 60 GB", - "main_ip": "192.168.100.50", - "vcpu_count": "2", - "location": "New Jersey", - "DCID": "1", - "default_password": "nreqnusibni", - "date_created": "2014-10-13 14:45:41", - "pending_charges": "3.72", - "status": "active", - "cost_per_month": "73.25", - "current_bandwidth_gb": 957.457, - "allowed_bandwidth_gb": "1000", - "netmask_v4": "255.255.255.248", - "gateway_v4": "123.123.123.1", - "power_status": "halted", - "server_state": "ok", - "VPSPLANID": "28", - "v6_network": "2001:DB8:1000::", - "v6_main_ip": "2001:DB8:1000::100", - "v6_network_size": "64", - "v6_networks": [ - { - "v6_network": "2001:DB8:1000::", - "v6_main_ip": "2001:DB8:1000::100", - "v6_network_size": "64" - } - ], - "label": "my failed server", - "internal_ip": "10.99.0.10", - "kvm_url": "https://my.vultr.com/subs/novnc/api.php?data=eawxFVZw2mXnhGUV", - "auto_backups": "no", - "tag": "mytag", - "OSID": "127", - "APPID": "0", - "FIREWALLGROUPID": "0" - }, - "555555": { - "SUBID": "555555", - "os": "CentOS 7 x64", - "ram": "1024 MB", - "disk": "Virtual 30 GB", - "main_ip": "192.168.250.50", - "vcpu_count": "1", - "location": "London", - "DCID": "7", - "default_password": "password", - "date_created": "2014-10-15 14:45:41", - "pending_charges": "5.45", - "status": "active", - "cost_per_month": "73.25", - "current_bandwidth_gb": 57.457, - "allowed_bandwidth_gb": "100", - "netmask_v4": "255.255.255.248", - "gateway_v4": "123.123.123.1", - "power_status": "halted", - "server_state": "ok", - "VPSPLANID": "28", - "v6_network": "2001:DB8:1000::", - "v6_main_ip": "2001:DB8:1000::100", - "v6_network_size": "64", - "v6_networks": [ - { - "v6_network": "2001:DB8:1000::", - "v6_main_ip": "2001:DB8:1000::100", - "v6_network_size": "64" - } - ], - "label": "Another Server", - "internal_ip": "10.99.0.10", - "kvm_url": "https://my.vultr.com/subs/novnc/api.php?data=eawxFVZw2mXnhGUV", - "auto_backups": "no", - "tag": "mytag", - "OSID": "127", - "APPID": "0", - "FIREWALLGROUPID": "0" - } -} diff --git a/tests/components/vultr/test_binary_sensor.py b/tests/components/vultr/test_binary_sensor.py deleted file mode 100644 index f6b46b54d25baf..00000000000000 --- a/tests/components/vultr/test_binary_sensor.py +++ /dev/null @@ -1,104 +0,0 @@ -"""Test the Vultr binary sensor platform.""" - -import pytest -import voluptuous as vol - -from homeassistant.components import vultr as base_vultr -from homeassistant.components.vultr import ( - ATTR_ALLOWED_BANDWIDTH, - ATTR_AUTO_BACKUPS, - ATTR_COST_PER_MONTH, - ATTR_CREATED_AT, - ATTR_IPV4_ADDRESS, - ATTR_SUBSCRIPTION_ID, - CONF_SUBSCRIPTION, - binary_sensor as vultr, -) -from homeassistant.const import CONF_NAME, CONF_PLATFORM -from homeassistant.core import HomeAssistant - -CONFIGS = [ - {CONF_SUBSCRIPTION: "576965", CONF_NAME: "A Server"}, - {CONF_SUBSCRIPTION: "123456", CONF_NAME: "Failed Server"}, - {CONF_SUBSCRIPTION: "555555", CONF_NAME: vultr.DEFAULT_NAME}, -] - - -@pytest.mark.usefixtures("valid_config") -def test_binary_sensor(hass: HomeAssistant) -> None: - """Test successful instance.""" - hass_devices = [] - - def add_entities(devices, action): - """Mock add devices.""" - for device in devices: - device.hass = hass - hass_devices.append(device) - - # Setup each of our test configs - for config in CONFIGS: - vultr.setup_platform(hass, config, add_entities, None) - - assert len(hass_devices) == 3 - - for device in hass_devices: - # Test pre data retrieval - if device.subscription == "555555": - assert device.name == "Vultr {}" - - device.update() - device_attrs = device.extra_state_attributes - - if device.subscription == "555555": - assert device.name == "Vultr Another Server" - - if device.name == "A Server": - assert device.is_on is True - assert device.device_class == "power" - assert device.state == "on" - assert device.icon == "mdi:server" - assert device_attrs[ATTR_ALLOWED_BANDWIDTH] == "1000" - assert device_attrs[ATTR_AUTO_BACKUPS] == "yes" - assert device_attrs[ATTR_IPV4_ADDRESS] == "123.123.123.123" - assert device_attrs[ATTR_COST_PER_MONTH] == "10.05" - assert device_attrs[ATTR_CREATED_AT] == "2013-12-19 14:45:41" - assert device_attrs[ATTR_SUBSCRIPTION_ID] == "576965" - elif device.name == "Failed Server": - assert device.is_on is False - assert device.state == "off" - assert device.icon == "mdi:server-off" - assert device_attrs[ATTR_ALLOWED_BANDWIDTH] == "1000" - assert device_attrs[ATTR_AUTO_BACKUPS] == "no" - assert device_attrs[ATTR_IPV4_ADDRESS] == "192.168.100.50" - assert device_attrs[ATTR_COST_PER_MONTH] == "73.25" - assert device_attrs[ATTR_CREATED_AT] == "2014-10-13 14:45:41" - assert device_attrs[ATTR_SUBSCRIPTION_ID] == "123456" - - -def test_invalid_sensor_config() -> None: - """Test config type failures.""" - with pytest.raises(vol.Invalid): # No subs - vultr.PLATFORM_SCHEMA({CONF_PLATFORM: base_vultr.DOMAIN}) - - -@pytest.mark.usefixtures("valid_config") -def test_invalid_sensors(hass: HomeAssistant) -> None: - """Test the VultrBinarySensor fails.""" - hass_devices = [] - - def add_entities(devices, action): - """Mock add devices.""" - for device in devices: - device.hass = hass - hass_devices.append(device) - - bad_conf = {} # No subscription - - vultr.setup_platform(hass, bad_conf, add_entities, None) - - bad_conf = { - CONF_NAME: "Missing Server", - CONF_SUBSCRIPTION: "555555", - } # Sub not associated with API key (not in server_list) - - vultr.setup_platform(hass, bad_conf, add_entities, None) diff --git a/tests/components/vultr/test_init.py b/tests/components/vultr/test_init.py deleted file mode 100644 index 8c5ec51f584a93..00000000000000 --- a/tests/components/vultr/test_init.py +++ /dev/null @@ -1,30 +0,0 @@ -"""The tests for the Vultr component.""" - -from copy import deepcopy -import json -from unittest.mock import patch - -from homeassistant import setup -from homeassistant.components import vultr -from homeassistant.core import HomeAssistant - -from .const import VALID_CONFIG - -from tests.common import load_fixture - - -def test_setup(hass: HomeAssistant) -> None: - """Test successful setup.""" - with patch( - "vultr.Vultr.server_list", - return_value=json.loads(load_fixture("server_list.json", "vultr")), - ): - response = vultr.setup(hass, VALID_CONFIG) - assert response - - -async def test_setup_no_api_key(hass: HomeAssistant) -> None: - """Test failed setup with missing API Key.""" - conf = deepcopy(VALID_CONFIG) - del conf["vultr"]["api_key"] - assert not await setup.async_setup_component(hass, vultr.DOMAIN, conf) diff --git a/tests/components/vultr/test_sensor.py b/tests/components/vultr/test_sensor.py deleted file mode 100644 index 65be23fc1683ef..00000000000000 --- a/tests/components/vultr/test_sensor.py +++ /dev/null @@ -1,134 +0,0 @@ -"""The tests for the Vultr sensor platform.""" - -import pytest -import voluptuous as vol - -from homeassistant.components import vultr as base_vultr -from homeassistant.components.vultr import CONF_SUBSCRIPTION, sensor as vultr -from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, - CONF_NAME, - CONF_PLATFORM, - UnitOfInformation, -) -from homeassistant.core import HomeAssistant - -CONFIGS = [ - { - CONF_NAME: vultr.DEFAULT_NAME, - CONF_SUBSCRIPTION: "576965", - CONF_MONITORED_CONDITIONS: vultr.SENSOR_KEYS, - }, - { - CONF_NAME: "Server {}", - CONF_SUBSCRIPTION: "123456", - CONF_MONITORED_CONDITIONS: vultr.SENSOR_KEYS, - }, - { - CONF_NAME: "VPS Charges", - CONF_SUBSCRIPTION: "555555", - CONF_MONITORED_CONDITIONS: ["pending_charges"], - }, -] - - -@pytest.mark.usefixtures("valid_config") -def test_sensor(hass: HomeAssistant) -> None: - """Test the Vultr sensor class and methods.""" - hass_devices = [] - - def add_entities(devices, action): - """Mock add devices.""" - for device in devices: - device.hass = hass - hass_devices.append(device) - - for config in CONFIGS: - vultr.setup_platform(hass, config, add_entities, None) - - assert len(hass_devices) == 5 - - tested = 0 - - for device in hass_devices: - # Test pre update - if device.subscription == "576965": - assert device.name == vultr.DEFAULT_NAME - - device.update() - - if ( - device.unit_of_measurement == UnitOfInformation.GIGABYTES - ): # Test Bandwidth Used - if device.subscription == "576965": - assert device.name == "Vultr my new server Current Bandwidth Used" - assert device.icon == "mdi:chart-histogram" - assert device.state == 131.51 - assert device.icon == "mdi:chart-histogram" - tested += 1 - - elif device.subscription == "123456": - assert device.name == "Server Current Bandwidth Used" - assert device.state == 957.46 - tested += 1 - - elif device.unit_of_measurement == "US$": # Test Pending Charges - if device.subscription == "576965": # Default 'Vultr {} {}' - assert device.name == "Vultr my new server Pending Charges" - assert device.icon == "mdi:currency-usd" - assert device.state == 46.67 - assert device.icon == "mdi:currency-usd" - tested += 1 - - elif device.subscription == "123456": # Custom name with 1 {} - assert device.name == "Server Pending Charges" - assert device.state == 3.72 - tested += 1 - - elif device.subscription == "555555": # No {} in name - assert device.name == "VPS Charges" - assert device.state == 5.45 - tested += 1 - - assert tested == 5 - - -def test_invalid_sensor_config() -> None: - """Test config type failures.""" - with pytest.raises(vol.Invalid): # No subscription - vultr.PLATFORM_SCHEMA( - { - CONF_PLATFORM: base_vultr.DOMAIN, - CONF_MONITORED_CONDITIONS: vultr.SENSOR_KEYS, - } - ) - with pytest.raises(vol.Invalid): # Bad monitored_conditions - vultr.PLATFORM_SCHEMA( - { - CONF_PLATFORM: base_vultr.DOMAIN, - CONF_SUBSCRIPTION: "123456", - CONF_MONITORED_CONDITIONS: ["non-existent-condition"], - } - ) - - -@pytest.mark.usefixtures("valid_config") -def test_invalid_sensors(hass: HomeAssistant) -> None: - """Test the VultrSensor fails.""" - hass_devices = [] - - def add_entities(devices, action): - """Mock add devices.""" - for device in devices: - device.hass = hass - hass_devices.append(device) - - bad_conf = { - CONF_NAME: "Vultr {} {}", - CONF_SUBSCRIPTION: "", - CONF_MONITORED_CONDITIONS: vultr.SENSOR_KEYS, - } # No subs at all - - vultr.setup_platform(hass, bad_conf, add_entities, None) - - assert len(hass_devices) == 0 diff --git a/tests/components/vultr/test_switch.py b/tests/components/vultr/test_switch.py deleted file mode 100644 index 14c88d1e878df8..00000000000000 --- a/tests/components/vultr/test_switch.py +++ /dev/null @@ -1,161 +0,0 @@ -"""Test the Vultr switch platform.""" - -from __future__ import annotations - -import json -from unittest.mock import patch - -import pytest -import voluptuous as vol - -from homeassistant.components import vultr as base_vultr -from homeassistant.components.vultr import ( - ATTR_ALLOWED_BANDWIDTH, - ATTR_AUTO_BACKUPS, - ATTR_COST_PER_MONTH, - ATTR_CREATED_AT, - ATTR_IPV4_ADDRESS, - ATTR_SUBSCRIPTION_ID, - CONF_SUBSCRIPTION, - switch as vultr, -) -from homeassistant.const import CONF_NAME, CONF_PLATFORM -from homeassistant.core import HomeAssistant - -from tests.common import load_fixture - -CONFIGS = [ - {CONF_SUBSCRIPTION: "576965", CONF_NAME: "A Server"}, - {CONF_SUBSCRIPTION: "123456", CONF_NAME: "Failed Server"}, - {CONF_SUBSCRIPTION: "555555", CONF_NAME: vultr.DEFAULT_NAME}, -] - - -@pytest.fixture(name="hass_devices") -def load_hass_devices(hass: HomeAssistant): - """Load a valid config.""" - hass_devices = [] - - def add_entities(devices, action): - """Mock add devices.""" - for device in devices: - device.hass = hass - hass_devices.append(device) - - # Setup each of our test configs - for config in CONFIGS: - vultr.setup_platform(hass, config, add_entities, None) - - return hass_devices - - -@pytest.mark.usefixtures("valid_config") -def test_switch(hass: HomeAssistant, hass_devices: list[vultr.VultrSwitch]) -> None: - """Test successful instance.""" - - assert len(hass_devices) == 3 - - tested = 0 - - for device in hass_devices: - if device.subscription == "555555": - assert device.name == "Vultr {}" - tested += 1 - - device.update() - device_attrs = device.extra_state_attributes - - if device.subscription == "555555": - assert device.name == "Vultr Another Server" - tested += 1 - - if device.name == "A Server": - assert device.is_on is True - assert device.state == "on" - assert device.icon == "mdi:server" - assert device_attrs[ATTR_ALLOWED_BANDWIDTH] == "1000" - assert device_attrs[ATTR_AUTO_BACKUPS] == "yes" - assert device_attrs[ATTR_IPV4_ADDRESS] == "123.123.123.123" - assert device_attrs[ATTR_COST_PER_MONTH] == "10.05" - assert device_attrs[ATTR_CREATED_AT] == "2013-12-19 14:45:41" - assert device_attrs[ATTR_SUBSCRIPTION_ID] == "576965" - tested += 1 - - elif device.name == "Failed Server": - assert device.is_on is False - assert device.state == "off" - assert device.icon == "mdi:server-off" - assert device_attrs[ATTR_ALLOWED_BANDWIDTH] == "1000" - assert device_attrs[ATTR_AUTO_BACKUPS] == "no" - assert device_attrs[ATTR_IPV4_ADDRESS] == "192.168.100.50" - assert device_attrs[ATTR_COST_PER_MONTH] == "73.25" - assert device_attrs[ATTR_CREATED_AT] == "2014-10-13 14:45:41" - assert device_attrs[ATTR_SUBSCRIPTION_ID] == "123456" - tested += 1 - - assert tested == 4 - - -@pytest.mark.usefixtures("valid_config") -def test_turn_on(hass: HomeAssistant, hass_devices: list[vultr.VultrSwitch]) -> None: - """Test turning a subscription on.""" - with ( - patch( - "vultr.Vultr.server_list", - return_value=json.loads(load_fixture("server_list.json", "vultr")), - ), - patch("vultr.Vultr.server_start") as mock_start, - ): - for device in hass_devices: - if device.name == "Failed Server": - device.update() - device.turn_on() - - # Turn on - assert mock_start.call_count == 1 - - -@pytest.mark.usefixtures("valid_config") -def test_turn_off(hass: HomeAssistant, hass_devices: list[vultr.VultrSwitch]) -> None: - """Test turning a subscription off.""" - with ( - patch( - "vultr.Vultr.server_list", - return_value=json.loads(load_fixture("server_list.json", "vultr")), - ), - patch("vultr.Vultr.server_halt") as mock_halt, - ): - for device in hass_devices: - if device.name == "A Server": - device.update() - device.turn_off() - - # Turn off - assert mock_halt.call_count == 1 - - -def test_invalid_switch_config() -> None: - """Test config type failures.""" - with pytest.raises(vol.Invalid): # No subscription - vultr.PLATFORM_SCHEMA({CONF_PLATFORM: base_vultr.DOMAIN}) - - -@pytest.mark.usefixtures("valid_config") -def test_invalid_switches(hass: HomeAssistant) -> None: - """Test the VultrSwitch fails.""" - hass_devices = [] - - def add_entities(devices, action): - """Mock add devices.""" - hass_devices.extend(devices) - - bad_conf = {} # No subscription - - vultr.setup_platform(hass, bad_conf, add_entities, None) - - bad_conf = { - CONF_NAME: "Missing Server", - CONF_SUBSCRIPTION: "665544", - } # Sub not associated with API key (not in server_list) - - vultr.setup_platform(hass, bad_conf, add_entities, None) diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index c3dda537db03d7..6310c368fc473c 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -1168,7 +1168,7 @@ async def mock_restore_nvm(data: bytes, options: dict[str, bool] | None = None): @pytest.mark.usefixtures("supervisor", "addon_not_installed", "addon_info") -async def test_esphome_discovery( +async def test_esphome_discovery_intent_custom( hass: HomeAssistant, install_addon: AsyncMock, set_addon_options: AsyncMock, @@ -1290,6 +1290,82 @@ async def test_esphome_discovery( assert len(mock_setup_entry.mock_calls) == 1 +@pytest.mark.usefixtures("supervisor", "addon_installed", "addon_running", "addon_info") +async def test_esphome_discovery_intent_recommended( + hass: HomeAssistant, + set_addon_options: AsyncMock, + addon_options: dict, +) -> None: + """Test ESPHome discovery success path.""" + addon_options.update( + { + CONF_ADDON_DEVICE: "/dev/ttyUSB0", + CONF_ADDON_SOCKET: None, + "s0_legacy_key": "new123", + "s2_access_control_key": "new456", + "s2_authenticated_key": "new789", + "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", + } + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ESPHOME}, + data=ESPHOME_DISCOVERY_INFO, + ) + + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "installation_type" + assert result["menu_options"] == ["intent_recommended", "intent_custom"] + + with ( + patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_recommended"} + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == TITLE + assert result["result"].unique_id == str(ESPHOME_DISCOVERY_INFO.zwave_home_id) + assert result["data"] == { + "url": "ws://host1:3001", + "usb_path": None, + "socket_path": "esphome://192.168.1.100:6053", + "s0_legacy_key": "new123", + "s2_access_control_key": "new456", + "s2_authenticated_key": "new789", + "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", + "use_addon": True, + "integration_created_addon": False, + } + assert set_addon_options.call_args == call( + "core_zwave_js", + AddonsOptions( + config={ + "socket": "esphome://192.168.1.100:6053", + "s0_legacy_key": "new123", + "s2_access_control_key": "new456", + "s2_authenticated_key": "new789", + "s2_unauthenticated_key": "new987", + "lr_s2_access_control_key": "new654", + "lr_s2_authenticated_key": "new321", + } + ), + ) + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + @pytest.mark.usefixtures("supervisor", "addon_installed", "addon_info") async def test_esphome_discovery_already_configured( hass: HomeAssistant,