mirror of
https://github.com/home-assistant/core
synced 2024-07-21 10:44:07 +00:00
Remove senseme integration (#94363)
This commit is contained in:
parent
50e36fbdda
commit
878d41a472
|
@ -1046,12 +1046,6 @@ omit =
|
|||
homeassistant/components/sense/__init__.py
|
||||
homeassistant/components/sense/binary_sensor.py
|
||||
homeassistant/components/sense/sensor.py
|
||||
homeassistant/components/senseme/__init__.py
|
||||
homeassistant/components/senseme/discovery.py
|
||||
homeassistant/components/senseme/entity.py
|
||||
homeassistant/components/senseme/fan.py
|
||||
homeassistant/components/senseme/light.py
|
||||
homeassistant/components/senseme/switch.py
|
||||
homeassistant/components/senz/__init__.py
|
||||
homeassistant/components/senz/api.py
|
||||
homeassistant/components/senz/climate.py
|
||||
|
|
|
@ -277,7 +277,6 @@ homeassistant.components.scene.*
|
|||
homeassistant.components.schedule.*
|
||||
homeassistant.components.scrape.*
|
||||
homeassistant.components.select.*
|
||||
homeassistant.components.senseme.*
|
||||
homeassistant.components.sensibo.*
|
||||
homeassistant.components.sensirion_ble.*
|
||||
homeassistant.components.sensor.*
|
||||
|
|
|
@ -1079,8 +1079,6 @@ build.json @home-assistant/supervisor
|
|||
/tests/components/select/ @home-assistant/core
|
||||
/homeassistant/components/sense/ @kbickar
|
||||
/tests/components/sense/ @kbickar
|
||||
/homeassistant/components/senseme/ @mikelawrence @bdraco
|
||||
/tests/components/senseme/ @mikelawrence @bdraco
|
||||
/homeassistant/components/sensibo/ @andrey-git @gjohansson-ST
|
||||
/tests/components/sensibo/ @andrey-git @gjohansson-ST
|
||||
/homeassistant/components/sensirion_ble/ @akx
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
"""The SenseME integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from aiosenseme import async_get_device_by_device_info
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import CONF_INFO, DOMAIN, PLATFORMS, UPDATE_RATE
|
||||
from .discovery import async_start_discovery
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up SenseME from a config entry."""
|
||||
async_start_discovery(hass)
|
||||
|
||||
status, device = await async_get_device_by_device_info(
|
||||
info=entry.data[CONF_INFO], start_first=True, refresh_minutes=UPDATE_RATE
|
||||
)
|
||||
if not status:
|
||||
device.stop()
|
||||
raise ConfigEntryNotReady(f"Connect to address {device.address} failed")
|
||||
|
||||
await device.async_update(not status)
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = device
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
hass.data[DOMAIN][entry.entry_id].stop()
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
|
@ -1,41 +0,0 @@
|
|||
"""Support for Big Ass Fans SenseME occupancy sensor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from aiosenseme import SensemeDevice
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import SensemeEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: config_entries.ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up SenseME occupancy sensors."""
|
||||
device = hass.data[DOMAIN][entry.entry_id]
|
||||
if device.has_sensor:
|
||||
async_add_entities([HASensemeOccupancySensor(device)])
|
||||
|
||||
|
||||
class HASensemeOccupancySensor(SensemeEntity, BinarySensorEntity):
|
||||
"""Representation of a Big Ass Fans SenseME occupancy sensor."""
|
||||
|
||||
def __init__(self, device: SensemeDevice) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(device, f"{device.name} Occupancy")
|
||||
self._attr_unique_id = f"{self._device.uuid}-SENSOR"
|
||||
self._attr_device_class = BinarySensorDeviceClass.OCCUPANCY
|
||||
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update attrs from device."""
|
||||
self._attr_is_on = self._device.motion_detected
|
|
@ -1,148 +0,0 @@
|
|||
"""Config flow for SenseME."""
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
from typing import Any
|
||||
|
||||
from aiosenseme import SensemeDevice, async_get_device_by_ip_address
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components import dhcp
|
||||
from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_ID
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.typing import DiscoveryInfoType
|
||||
|
||||
from .const import CONF_HOST_MANUAL, CONF_INFO, DOMAIN
|
||||
from .discovery import async_discover, async_get_discovered_device
|
||||
|
||||
DISCOVER_TIMEOUT = 5
|
||||
|
||||
|
||||
class SensemeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle SenseME discovery config flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the SenseME config flow."""
|
||||
self._discovered_devices: list[SensemeDevice] | None = None
|
||||
self._discovered_device: SensemeDevice | None = None
|
||||
|
||||
async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult:
|
||||
"""Handle discovery via dhcp."""
|
||||
# If discovery is already running, it takes precedence since its more efficient
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="already_configured")
|
||||
if device := await async_get_device_by_ip_address(discovery_info.ip):
|
||||
device.stop()
|
||||
if device is None or not device.uuid:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
await self.async_set_unique_id(device.uuid)
|
||||
self._discovered_device = device
|
||||
return await self.async_step_discovery_confirm()
|
||||
|
||||
async def async_step_integration_discovery(
|
||||
self, discovery_info: DiscoveryInfoType
|
||||
) -> FlowResult:
|
||||
"""Handle integration discovery."""
|
||||
uuid = discovery_info[CONF_ID]
|
||||
device = async_get_discovered_device(self.hass, discovery_info[CONF_ID])
|
||||
host = device.address
|
||||
await self.async_set_unique_id(uuid)
|
||||
for entry in self._async_current_entries(include_ignore=False):
|
||||
if entry.data[CONF_INFO]["address"] == host:
|
||||
return self.async_abort(reason="already_configured")
|
||||
if entry.unique_id != uuid:
|
||||
continue
|
||||
self.hass.config_entries.async_update_entry(
|
||||
entry, data={CONF_INFO: {**entry.data[CONF_INFO], "address": host}}
|
||||
)
|
||||
return self.async_abort(reason="already_configured")
|
||||
self._discovered_device = device
|
||||
return await self.async_step_discovery_confirm()
|
||||
|
||||
async def async_step_discovery_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Confirm discovery."""
|
||||
device = self._discovered_device
|
||||
assert device is not None
|
||||
|
||||
if user_input is not None:
|
||||
return await self._async_entry_for_device(device)
|
||||
placeholders = {
|
||||
"name": device.name,
|
||||
"model": device.model,
|
||||
"host": device.address,
|
||||
}
|
||||
self.context["title_placeholders"] = placeholders
|
||||
return self.async_show_form(
|
||||
step_id="discovery_confirm", description_placeholders=placeholders
|
||||
)
|
||||
|
||||
async def _async_entry_for_device(self, device: SensemeDevice) -> FlowResult:
|
||||
"""Create a config entry for a device."""
|
||||
await self.async_set_unique_id(device.uuid, raise_on_progress=False)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=device.name,
|
||||
data={CONF_INFO: device.get_device_info},
|
||||
)
|
||||
|
||||
async def async_step_manual(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle manual entry of an ip address."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
host = user_input[CONF_HOST]
|
||||
try:
|
||||
ipaddress.ip_address(host)
|
||||
except ValueError:
|
||||
errors[CONF_HOST] = "invalid_host"
|
||||
else:
|
||||
if device := await async_get_device_by_ip_address(host):
|
||||
device.stop()
|
||||
return await self._async_entry_for_device(device)
|
||||
|
||||
errors[CONF_HOST] = "cannot_connect"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="manual",
|
||||
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
if self._discovered_devices is None:
|
||||
self._discovered_devices = await async_discover(self.hass, DISCOVER_TIMEOUT)
|
||||
current_ids = self._async_current_ids()
|
||||
device_selection = {
|
||||
device.uuid: device.name
|
||||
for device in self._discovered_devices
|
||||
if device.uuid not in current_ids
|
||||
}
|
||||
|
||||
if not device_selection:
|
||||
return await self.async_step_manual(user_input=None)
|
||||
|
||||
device_selection[None] = CONF_HOST_MANUAL
|
||||
|
||||
if user_input is not None:
|
||||
if user_input[CONF_DEVICE] is None:
|
||||
return await self.async_step_manual()
|
||||
|
||||
for device in self._discovered_devices:
|
||||
if device.uuid == user_input[CONF_DEVICE]:
|
||||
return await self._async_entry_for_device(device)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{vol.Required(CONF_DEVICE): vol.In(device_selection)}
|
||||
),
|
||||
)
|
|
@ -1,29 +0,0 @@
|
|||
"""Constants for the SenseME integration."""
|
||||
|
||||
|
||||
from homeassistant.const import Platform
|
||||
|
||||
DOMAIN = "senseme"
|
||||
|
||||
# Periodic fan update rate in minutes
|
||||
UPDATE_RATE = 1
|
||||
|
||||
# data storage
|
||||
CONF_INFO = "info"
|
||||
CONF_HOST_MANUAL = "IP Address"
|
||||
DISCOVERY = "discovery"
|
||||
|
||||
# Fan Preset Modes
|
||||
PRESET_MODE_WHOOSH = "Whoosh"
|
||||
|
||||
# Fan Directions
|
||||
SENSEME_DIRECTION_FORWARD = "FWD"
|
||||
SENSEME_DIRECTION_REVERSE = "REV"
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.FAN,
|
||||
Platform.LIGHT,
|
||||
Platform.SELECT,
|
||||
Platform.SWITCH,
|
||||
]
|
|
@ -1,64 +0,0 @@
|
|||
"""The SenseME integration discovery."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from aiosenseme import SensemeDevice, SensemeDiscovery
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_ID
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import discovery_flow
|
||||
|
||||
from .const import DISCOVERY, DOMAIN
|
||||
|
||||
|
||||
@callback
|
||||
def async_start_discovery(hass: HomeAssistant) -> bool:
|
||||
"""Start discovery if its not already running."""
|
||||
domain_data = hass.data.setdefault(DOMAIN, {})
|
||||
if DISCOVERY in domain_data:
|
||||
return False # already running
|
||||
discovery = domain_data[DISCOVERY] = SensemeDiscovery(False)
|
||||
discovery.add_callback(lambda devices: async_trigger_discovery(hass, devices))
|
||||
discovery.start()
|
||||
return True # started
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_discovered_device(hass: HomeAssistant, uuid: str) -> SensemeDevice:
|
||||
"""Return a discovered device."""
|
||||
discovery: SensemeDiscovery = hass.data[DOMAIN][DISCOVERY]
|
||||
devices: list[SensemeDevice] = discovery.devices
|
||||
for discovered_device in devices:
|
||||
if discovered_device.uuid == uuid:
|
||||
return discovered_device
|
||||
raise RuntimeError("Discovered device unexpectedly disappeared")
|
||||
|
||||
|
||||
async def async_discover(hass: HomeAssistant, timeout: float) -> list[SensemeDevice]:
|
||||
"""Discover devices or restart it if its already running."""
|
||||
started = async_start_discovery(hass)
|
||||
discovery: SensemeDiscovery = hass.data[DOMAIN][DISCOVERY]
|
||||
if not started: # already running
|
||||
discovery.stop()
|
||||
discovery.start()
|
||||
await asyncio.sleep(timeout)
|
||||
devices: list[SensemeDevice] = discovery.devices
|
||||
return devices
|
||||
|
||||
|
||||
@callback
|
||||
def async_trigger_discovery(
|
||||
hass: HomeAssistant,
|
||||
discovered_devices: list[SensemeDevice],
|
||||
) -> None:
|
||||
"""Trigger config flows for discovered devices."""
|
||||
for device in discovered_devices:
|
||||
if device.uuid:
|
||||
discovery_flow.async_create_flow(
|
||||
hass,
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={CONF_ID: device.uuid},
|
||||
)
|
|
@ -1,47 +0,0 @@
|
|||
"""The SenseME integration entities."""
|
||||
from __future__ import annotations
|
||||
|
||||
from aiosenseme import SensemeDevice
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||
|
||||
|
||||
class SensemeEntity(Entity):
|
||||
"""Base class for senseme entities."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(self, device: SensemeDevice, name: str) -> None:
|
||||
"""Initialize the entity."""
|
||||
self._device = device
|
||||
self._attr_name = name
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, self._device.mac)},
|
||||
name=self._device.name,
|
||||
manufacturer="Big Ass Fans",
|
||||
model=self._device.model,
|
||||
sw_version=self._device.fw_version,
|
||||
suggested_area=self._device.room_name,
|
||||
)
|
||||
self._async_update_attrs()
|
||||
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update attrs from device."""
|
||||
self._attr_available = self._device.available
|
||||
|
||||
@callback
|
||||
def _async_update_from_device(self) -> None:
|
||||
"""Process an update from the device."""
|
||||
self._async_update_attrs()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Add data updated listener after this object has been initialized."""
|
||||
self._device.add_callback(self._async_update_from_device)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Remove data updated listener after this object has been initialized."""
|
||||
self._device.remove_callback(self._async_update_from_device)
|
|
@ -1,113 +0,0 @@
|
|||
"""Support for Big Ass Fans SenseME fan."""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
from aiosenseme import SensemeFan
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.fan import (
|
||||
DIRECTION_FORWARD,
|
||||
DIRECTION_REVERSE,
|
||||
FanEntity,
|
||||
FanEntityFeature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util.percentage import (
|
||||
percentage_to_ranged_value,
|
||||
ranged_value_to_percentage,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
PRESET_MODE_WHOOSH,
|
||||
SENSEME_DIRECTION_FORWARD,
|
||||
SENSEME_DIRECTION_REVERSE,
|
||||
)
|
||||
from .entity import SensemeEntity
|
||||
|
||||
SENSEME_DIRECTION_TO_HASS = {
|
||||
SENSEME_DIRECTION_FORWARD: DIRECTION_FORWARD,
|
||||
SENSEME_DIRECTION_REVERSE: DIRECTION_REVERSE,
|
||||
}
|
||||
HASS_DIRECTION_TO_SENSEME = {v: k for k, v in SENSEME_DIRECTION_TO_HASS.items()}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: config_entries.ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up SenseME fans."""
|
||||
device = hass.data[DOMAIN][entry.entry_id]
|
||||
if device.is_fan:
|
||||
async_add_entities([HASensemeFan(device)])
|
||||
|
||||
|
||||
class HASensemeFan(SensemeEntity, FanEntity):
|
||||
"""SenseME ceiling fan component."""
|
||||
|
||||
_attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.DIRECTION
|
||||
_attr_preset_modes = [PRESET_MODE_WHOOSH]
|
||||
|
||||
def __init__(self, device: SensemeFan) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(device, device.name)
|
||||
self._attr_speed_count = self._device.fan_speed_max
|
||||
self._attr_unique_id = f"{self._device.uuid}-FAN" # for legacy compat
|
||||
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update attrs from device."""
|
||||
self._attr_is_on = self._device.fan_on
|
||||
self._attr_current_direction = SENSEME_DIRECTION_TO_HASS.get(
|
||||
self._device.fan_dir, DIRECTION_FORWARD # None also means forward
|
||||
)
|
||||
if self._device.fan_speed is not None:
|
||||
self._attr_percentage = ranged_value_to_percentage(
|
||||
self._device.fan_speed_limits, self._device.fan_speed
|
||||
)
|
||||
else:
|
||||
self._attr_percentage = None
|
||||
whoosh = self._device.fan_whoosh_mode
|
||||
self._attr_preset_mode = PRESET_MODE_WHOOSH if whoosh else None
|
||||
super()._async_update_attrs()
|
||||
|
||||
async def async_set_percentage(self, percentage: int) -> None:
|
||||
"""Set the speed of the fan, as a percentage."""
|
||||
self._device.fan_speed = math.ceil(
|
||||
percentage_to_ranged_value(self._device.fan_speed_limits, percentage)
|
||||
)
|
||||
|
||||
async def async_turn_on(
|
||||
self,
|
||||
percentage: int | None = None,
|
||||
preset_mode: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Turn the fan on with a percentage or preset mode."""
|
||||
if preset_mode is not None:
|
||||
await self.async_set_preset_mode(preset_mode)
|
||||
elif percentage is None:
|
||||
self._device.fan_on = True
|
||||
else:
|
||||
await self.async_set_percentage(percentage)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the fan off."""
|
||||
self._device.fan_on = False
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set the preset mode of the fan."""
|
||||
if preset_mode != PRESET_MODE_WHOOSH:
|
||||
raise ValueError(f"Invalid preset mode: {preset_mode}")
|
||||
# Sleep mode must be off for Whoosh to work.
|
||||
if self._device.sleep_mode:
|
||||
self._device.sleep_mode = False
|
||||
self._device.fan_whoosh_mode = True
|
||||
|
||||
async def async_set_direction(self, direction: str) -> None:
|
||||
"""Set the direction of the fan."""
|
||||
self._device.fan_dir = HASS_DIRECTION_TO_SENSEME[direction]
|
|
@ -1,110 +0,0 @@
|
|||
"""Support for Big Ass Fans SenseME light."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aiosenseme import SensemeDevice
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_TEMP,
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util.color import (
|
||||
color_temperature_kelvin_to_mired,
|
||||
color_temperature_mired_to_kelvin,
|
||||
)
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import SensemeEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: config_entries.ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up SenseME lights."""
|
||||
device = hass.data[DOMAIN][entry.entry_id]
|
||||
if not device.has_light:
|
||||
return
|
||||
if device.is_light:
|
||||
async_add_entities([HASensemeStandaloneLight(device)])
|
||||
else:
|
||||
async_add_entities([HASensemeFanLight(device)])
|
||||
|
||||
|
||||
class HASensemeLight(SensemeEntity, LightEntity):
|
||||
"""Representation of a Big Ass Fans SenseME light."""
|
||||
|
||||
def __init__(self, device: SensemeDevice, name: str) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(device, name)
|
||||
self._attr_unique_id = f"{device.uuid}-LIGHT" # for legacy compat
|
||||
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update attrs from device."""
|
||||
self._attr_is_on = self._device.light_on
|
||||
if self._device.light_brightness is not None:
|
||||
self._attr_brightness = int(min(255, self._device.light_brightness * 16))
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the light."""
|
||||
if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None:
|
||||
# set the brightness, which will also turn on/off light
|
||||
if brightness == 255:
|
||||
brightness = 256 # this will end up as 16 which is max
|
||||
self._device.light_brightness = int(brightness / 16)
|
||||
else:
|
||||
self._device.light_on = True
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the light."""
|
||||
self._device.light_on = False
|
||||
|
||||
|
||||
class HASensemeFanLight(HASensemeLight):
|
||||
"""Representation of a Big Ass Fans SenseME light on a fan."""
|
||||
|
||||
def __init__(self, device: SensemeDevice) -> None:
|
||||
"""Init a fan light."""
|
||||
super().__init__(device, device.name)
|
||||
self._attr_supported_color_modes = {ColorMode.BRIGHTNESS}
|
||||
self._attr_color_mode = ColorMode.BRIGHTNESS
|
||||
|
||||
|
||||
class HASensemeStandaloneLight(HASensemeLight):
|
||||
"""Representation of a Big Ass Fans SenseME light."""
|
||||
|
||||
def __init__(self, device: SensemeDevice) -> None:
|
||||
"""Init a standalone light."""
|
||||
super().__init__(device, f"{device.name} Light")
|
||||
self._attr_supported_color_modes = {ColorMode.COLOR_TEMP}
|
||||
self._attr_color_mode = ColorMode.COLOR_TEMP
|
||||
self._attr_min_mireds = color_temperature_kelvin_to_mired(
|
||||
device.light_color_temp_max
|
||||
)
|
||||
self._attr_max_mireds = color_temperature_kelvin_to_mired(
|
||||
device.light_color_temp_min
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update attrs from device."""
|
||||
super()._async_update_attrs()
|
||||
self._attr_color_temp = color_temperature_kelvin_to_mired(
|
||||
self._device.light_color_temp
|
||||
)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the light."""
|
||||
if (color_temp := kwargs.get(ATTR_COLOR_TEMP)) is not None:
|
||||
self._device.light_color_temp = color_temperature_mired_to_kelvin(
|
||||
color_temp
|
||||
)
|
||||
await super().async_turn_on(**kwargs)
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"domain": "senseme",
|
||||
"name": "SenseME",
|
||||
"codeowners": ["@mikelawrence", "@bdraco"],
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
{
|
||||
"registered_devices": true
|
||||
},
|
||||
{
|
||||
"macaddress": "20F85E*"
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/senseme",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiosenseme"],
|
||||
"requirements": ["aiosenseme==0.6.1"]
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
"""Support for Big Ass Fans SenseME selects."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from aiosenseme import SensemeFan
|
||||
from aiosenseme.device import SensemeDevice
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import SensemeEntity
|
||||
|
||||
SMART_MODE_TO_HASS = {
|
||||
"OFF": "Off",
|
||||
"COOLING": "Cooling",
|
||||
"HEATING": "Heating",
|
||||
"FOLLOWTSTAT": "Follow Thermostat",
|
||||
}
|
||||
HASS_TO_SMART_MODE = {v: k for k, v in SMART_MODE_TO_HASS.items()}
|
||||
|
||||
|
||||
@dataclass
|
||||
class SenseMESelectEntityDescriptionMixin:
|
||||
"""Mixin for required keys."""
|
||||
|
||||
value_fn: Callable[[SensemeFan], str]
|
||||
set_fn: Callable[[SensemeFan, str], None]
|
||||
|
||||
|
||||
@dataclass
|
||||
class SenseMESelectEntityDescription(
|
||||
SelectEntityDescription, SenseMESelectEntityDescriptionMixin
|
||||
):
|
||||
"""Describes SenseME select entity."""
|
||||
|
||||
|
||||
def _set_smart_mode(device: SensemeDevice, value: str) -> None:
|
||||
device.fan_smartmode = HASS_TO_SMART_MODE[value]
|
||||
|
||||
|
||||
FAN_SELECTS = [
|
||||
SenseMESelectEntityDescription(
|
||||
key="smart_mode",
|
||||
name="Smart Mode",
|
||||
value_fn=lambda device: SMART_MODE_TO_HASS[device.fan_smartmode],
|
||||
set_fn=_set_smart_mode,
|
||||
options=list(SMART_MODE_TO_HASS.values()),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: config_entries.ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up SenseME fan selects."""
|
||||
device = hass.data[DOMAIN][entry.entry_id]
|
||||
if device.is_fan:
|
||||
async_add_entities(
|
||||
HASensemeSelect(device, description) for description in FAN_SELECTS
|
||||
)
|
||||
|
||||
|
||||
class HASensemeSelect(SensemeEntity, SelectEntity):
|
||||
"""SenseME select component."""
|
||||
|
||||
entity_description: SenseMESelectEntityDescription
|
||||
|
||||
def __init__(
|
||||
self, device: SensemeFan, description: SenseMESelectEntityDescription
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
self.entity_description = description
|
||||
super().__init__(device, f"{device.name} {description.name}")
|
||||
self._attr_unique_id = f"{self._device.uuid}-{description.key}"
|
||||
|
||||
@property
|
||||
def current_option(self) -> str:
|
||||
"""Return the current value."""
|
||||
return self.entity_description.value_fn(self._device)
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Set the option."""
|
||||
self.entity_description.set_fn(self._device, option)
|
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"config": {
|
||||
"flow_title": "{name} - {model} ({host})",
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Select a device, or choose 'IP Address' to manually enter an IP Address.",
|
||||
"data": {
|
||||
"device": "Device"
|
||||
}
|
||||
},
|
||||
"discovery_confirm": {
|
||||
"description": "Do you want to set up {name} - {model} ({host})?"
|
||||
},
|
||||
"manual": {
|
||||
"description": "Enter an IP Address.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"error": {
|
||||
"invalid_host": "[%key:common::config_flow::error::invalid_host%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,138 +0,0 @@
|
|||
"""Support for Big Ass Fans SenseME switch."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, cast
|
||||
|
||||
from aiosenseme import SensemeFan
|
||||
from aiosenseme.device import SensemeDevice
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.switch import (
|
||||
SwitchDeviceClass,
|
||||
SwitchEntity,
|
||||
SwitchEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import SensemeEntity
|
||||
|
||||
|
||||
@dataclass
|
||||
class SenseMESwitchEntityDescriptionMixin:
|
||||
"""Mixin for required keys."""
|
||||
|
||||
value_fn: Callable[[SensemeFan], bool]
|
||||
set_fn: Callable[[SensemeFan, bool], None]
|
||||
|
||||
|
||||
@dataclass
|
||||
class SenseMESwitchEntityDescription(
|
||||
SwitchEntityDescription, SenseMESwitchEntityDescriptionMixin
|
||||
):
|
||||
"""Describes SenseME switch entity."""
|
||||
|
||||
|
||||
def _set_sleep_mode(device: SensemeDevice, value: bool) -> None:
|
||||
device.sleep_mode = value
|
||||
|
||||
|
||||
def _set_motion_fan_auto(device: SensemeDevice, value: bool) -> None:
|
||||
device.motion_fan_auto = value
|
||||
|
||||
|
||||
def _set_motion_light_auto(device: SensemeDevice, value: bool) -> None:
|
||||
device.motion_light_auto = value
|
||||
|
||||
|
||||
FAN_SWITCHES = [
|
||||
# Turning on sleep mode will disable Whoosh
|
||||
SenseMESwitchEntityDescription(
|
||||
key="sleep_mode",
|
||||
name="Sleep Mode",
|
||||
value_fn=lambda device: cast(bool, device.sleep_mode),
|
||||
set_fn=_set_sleep_mode,
|
||||
),
|
||||
SenseMESwitchEntityDescription(
|
||||
key="motion_fan_auto",
|
||||
name="Motion",
|
||||
value_fn=lambda device: cast(bool, device.motion_fan_auto),
|
||||
set_fn=_set_motion_fan_auto,
|
||||
),
|
||||
]
|
||||
|
||||
FAN_LIGHT_SWITCHES = [
|
||||
SenseMESwitchEntityDescription(
|
||||
key="motion_light_auto",
|
||||
name="Light Motion",
|
||||
value_fn=lambda device: cast(bool, device.motion_light_auto),
|
||||
set_fn=_set_motion_light_auto,
|
||||
),
|
||||
]
|
||||
|
||||
LIGHT_SWITCHES = [
|
||||
SenseMESwitchEntityDescription(
|
||||
key="sleep_mode",
|
||||
name="Sleep Mode",
|
||||
value_fn=lambda device: cast(bool, device.sleep_mode),
|
||||
set_fn=_set_sleep_mode,
|
||||
),
|
||||
SenseMESwitchEntityDescription(
|
||||
key="motion_light_auto",
|
||||
name="Motion",
|
||||
value_fn=lambda device: cast(bool, device.motion_light_auto),
|
||||
set_fn=_set_motion_light_auto,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: config_entries.ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up SenseME fans."""
|
||||
device = hass.data[DOMAIN][entry.entry_id]
|
||||
descriptions: list[SenseMESwitchEntityDescription] = []
|
||||
|
||||
if device.is_fan:
|
||||
descriptions.extend(FAN_SWITCHES)
|
||||
if device.has_light:
|
||||
descriptions.extend(FAN_LIGHT_SWITCHES)
|
||||
elif device.is_light:
|
||||
descriptions.extend(LIGHT_SWITCHES)
|
||||
|
||||
async_add_entities(
|
||||
HASensemeSwitch(device, description) for description in descriptions
|
||||
)
|
||||
|
||||
|
||||
class HASensemeSwitch(SensemeEntity, SwitchEntity):
|
||||
"""SenseME switch component."""
|
||||
|
||||
entity_description: SenseMESwitchEntityDescription
|
||||
|
||||
def __init__(
|
||||
self, device: SensemeFan, description: SenseMESwitchEntityDescription
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
self.entity_description = description
|
||||
self._attr_device_class = SwitchDeviceClass.SWITCH
|
||||
super().__init__(device, f"{device.name} {description.name}")
|
||||
self._attr_unique_id = f"{self._device.uuid}-SWITCH-{description.key}"
|
||||
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update attrs from device."""
|
||||
self._attr_is_on = self.entity_description.value_fn(self._device)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the switch."""
|
||||
self.entity_description.set_fn(self._device, True)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the switch."""
|
||||
self.entity_description.set_fn(self._device, False)
|
|
@ -391,7 +391,6 @@ FLOWS = {
|
|||
"screenlogic",
|
||||
"season",
|
||||
"sense",
|
||||
"senseme",
|
||||
"sensibo",
|
||||
"sensirion_ble",
|
||||
"sensorpro",
|
||||
|
|
|
@ -501,14 +501,6 @@ DHCP: list[dict[str, str | bool]] = [
|
|||
"hostname": "sense-*",
|
||||
"macaddress": "A4D578*",
|
||||
},
|
||||
{
|
||||
"domain": "senseme",
|
||||
"registered_devices": True,
|
||||
},
|
||||
{
|
||||
"domain": "senseme",
|
||||
"macaddress": "20F85E*",
|
||||
},
|
||||
{
|
||||
"domain": "sensibo",
|
||||
"hostname": "sensibo*",
|
||||
|
|
|
@ -4829,12 +4829,6 @@
|
|||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"senseme": {
|
||||
"name": "SenseME",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
"sensibo": {
|
||||
"name": "Sensibo",
|
||||
"integration_type": "hub",
|
||||
|
|
10
mypy.ini
10
mypy.ini
|
@ -2532,16 +2532,6 @@ disallow_untyped_defs = true
|
|||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.senseme.*]
|
||||
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.sensibo.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
|
|
@ -335,9 +335,6 @@ aioridwell==2023.01.0
|
|||
# homeassistant.components.ruuvi_gateway
|
||||
aioruuvigateway==0.1.0
|
||||
|
||||
# homeassistant.components.senseme
|
||||
aiosenseme==0.6.1
|
||||
|
||||
# homeassistant.components.senz
|
||||
aiosenz==1.0.0
|
||||
|
||||
|
|
|
@ -307,9 +307,6 @@ aioridwell==2023.01.0
|
|||
# homeassistant.components.ruuvi_gateway
|
||||
aioruuvigateway==0.1.0
|
||||
|
||||
# homeassistant.components.senseme
|
||||
aiosenseme==0.6.1
|
||||
|
||||
# homeassistant.components.senz
|
||||
aiosenz==1.0.0
|
||||
|
||||
|
|
|
@ -1,142 +0,0 @@
|
|||
"""Tests for the SenseME integration."""
|
||||
|
||||
from contextlib import contextmanager
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from aiosenseme import SensemeDevice, SensemeDiscovery
|
||||
|
||||
from homeassistant.components.senseme import config_flow
|
||||
|
||||
MOCK_NAME = "Haiku Fan"
|
||||
MOCK_UUID = "77a6b7b3-925d-4695-a415-76d76dca4444"
|
||||
MOCK_ADDRESS = "127.0.0.1"
|
||||
MOCK_MAC = "20:F8:5E:92:5A:75"
|
||||
|
||||
|
||||
def _mock_device():
|
||||
device = MagicMock(auto_spec=SensemeDevice)
|
||||
device.async_update = AsyncMock()
|
||||
device.model = "Haiku Fan"
|
||||
device.fan_speed_max = 7
|
||||
device.mac = "aa:bb:cc:dd:ee:ff"
|
||||
device.fan_dir = "REV"
|
||||
device.has_light = True
|
||||
device.is_light = False
|
||||
device.light_brightness = 50
|
||||
device.room_name = "Main"
|
||||
device.room_type = "Main"
|
||||
device.fw_version = "1"
|
||||
device.fan_autocomfort = "COOLING"
|
||||
device.fan_smartmode = "OFF"
|
||||
device.fan_whoosh_mode = "on"
|
||||
device.name = MOCK_NAME
|
||||
device.uuid = MOCK_UUID
|
||||
device.address = MOCK_ADDRESS
|
||||
device.get_device_info = {
|
||||
"name": MOCK_NAME,
|
||||
"uuid": MOCK_UUID,
|
||||
"mac": MOCK_ADDRESS,
|
||||
"address": MOCK_ADDRESS,
|
||||
"base_model": "FAN,HAIKU,HSERIES",
|
||||
"has_light": False,
|
||||
"has_sensor": True,
|
||||
"is_fan": True,
|
||||
"is_light": False,
|
||||
}
|
||||
return device
|
||||
|
||||
|
||||
device_alternate_ip = MagicMock(auto_spec=SensemeDevice)
|
||||
device_alternate_ip.async_update = AsyncMock()
|
||||
device_alternate_ip.model = "Haiku Fan"
|
||||
device_alternate_ip.fan_speed_max = 7
|
||||
device_alternate_ip.mac = "aa:bb:cc:dd:ee:ff"
|
||||
device_alternate_ip.fan_dir = "REV"
|
||||
device_alternate_ip.room_name = "Main"
|
||||
device_alternate_ip.room_type = "Main"
|
||||
device_alternate_ip.fw_version = "1"
|
||||
device_alternate_ip.fan_autocomfort = "on"
|
||||
device_alternate_ip.fan_smartmode = "on"
|
||||
device_alternate_ip.fan_whoosh_mode = "on"
|
||||
device_alternate_ip.name = MOCK_NAME
|
||||
device_alternate_ip.uuid = MOCK_UUID
|
||||
device_alternate_ip.address = "127.0.0.8"
|
||||
device_alternate_ip.get_device_info = {
|
||||
"name": MOCK_NAME,
|
||||
"uuid": MOCK_UUID,
|
||||
"mac": "20:F8:5E:92:5A:75",
|
||||
"address": "127.0.0.8",
|
||||
"base_model": "FAN,HAIKU,HSERIES",
|
||||
"has_light": False,
|
||||
"has_sensor": True,
|
||||
"is_fan": True,
|
||||
"is_light": False,
|
||||
}
|
||||
|
||||
|
||||
device2 = MagicMock(auto_spec=SensemeDevice)
|
||||
device2.async_update = AsyncMock()
|
||||
device2.model = "Haiku Fan"
|
||||
device2.fan_speed_max = 7
|
||||
device2.mac = "aa:bb:cc:dd:ee:ff"
|
||||
device2.fan_dir = "FWD"
|
||||
device2.room_name = "Main"
|
||||
device2.room_type = "Main"
|
||||
device2.fw_version = "1"
|
||||
device2.fan_autocomfort = "on"
|
||||
device2.fan_smartmode = "on"
|
||||
device2.fan_whoosh_mode = "on"
|
||||
device2.name = "Device 2"
|
||||
device2.uuid = "uuid2"
|
||||
device2.address = "127.0.0.2"
|
||||
device2.get_device_info = {
|
||||
"name": "Device 2",
|
||||
"uuid": "uuid2",
|
||||
"mac": "20:F8:5E:92:5A:76",
|
||||
"address": "127.0.0.2",
|
||||
"base_model": "FAN,HAIKU,HSERIES",
|
||||
"has_light": True,
|
||||
"has_sensor": True,
|
||||
"is_fan": True,
|
||||
"is_light": False,
|
||||
}
|
||||
|
||||
device_no_uuid = MagicMock(auto_spec=SensemeDevice)
|
||||
device_no_uuid.uuid = None
|
||||
|
||||
|
||||
MOCK_DEVICE = _mock_device()
|
||||
MOCK_DEVICE_ALTERNATE_IP = device_alternate_ip
|
||||
MOCK_DEVICE2 = device2
|
||||
MOCK_DEVICE_NO_UUID = device_no_uuid
|
||||
|
||||
|
||||
def _patch_discovery(device=None, no_device=None):
|
||||
"""Patch discovery."""
|
||||
mock_senseme_discovery = MagicMock(auto_spec=SensemeDiscovery)
|
||||
if not no_device:
|
||||
mock_senseme_discovery.devices = [device or MOCK_DEVICE]
|
||||
|
||||
@contextmanager
|
||||
def _patcher():
|
||||
with patch.object(config_flow, "DISCOVER_TIMEOUT", 0), patch(
|
||||
"homeassistant.components.senseme.discovery.SensemeDiscovery",
|
||||
return_value=mock_senseme_discovery,
|
||||
):
|
||||
yield
|
||||
|
||||
return _patcher()
|
||||
|
||||
|
||||
def _patch_device(device=None, no_device=False):
|
||||
async def _device_mocker(*args, **kwargs):
|
||||
if no_device:
|
||||
return False, None
|
||||
if device:
|
||||
return True, device
|
||||
return True, _mock_device()
|
||||
|
||||
return patch(
|
||||
"homeassistant.components.senseme.async_get_device_by_device_info",
|
||||
new=_device_mocker,
|
||||
)
|
|
@ -1,361 +0,0 @@
|
|||
"""Test the SenseME config flow."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components import dhcp
|
||||
from homeassistant.components.senseme.const import DOMAIN
|
||||
from homeassistant.const import CONF_HOST, CONF_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from . import (
|
||||
MOCK_ADDRESS,
|
||||
MOCK_DEVICE,
|
||||
MOCK_DEVICE2,
|
||||
MOCK_DEVICE_ALTERNATE_IP,
|
||||
MOCK_DEVICE_NO_UUID,
|
||||
MOCK_MAC,
|
||||
MOCK_UUID,
|
||||
_patch_discovery,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
DHCP_DISCOVERY = dhcp.DhcpServiceInfo(
|
||||
hostname="any",
|
||||
ip=MOCK_ADDRESS,
|
||||
macaddress=MOCK_MAC,
|
||||
)
|
||||
|
||||
|
||||
async def test_form_user(hass: HomeAssistant) -> None:
|
||||
"""Test we get the form as a user."""
|
||||
|
||||
with _patch_discovery(), patch(
|
||||
"homeassistant.components.senseme.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert not result["errors"]
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"device": MOCK_UUID,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == "Haiku Fan"
|
||||
assert result2["data"] == {
|
||||
"info": MOCK_DEVICE.get_device_info,
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_user_manual_entry(hass: HomeAssistant) -> None:
|
||||
"""Test we get the form as a user with a discovery but user chooses manual."""
|
||||
|
||||
with _patch_discovery():
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert not result["errors"]
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"device": None,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == FlowResultType.FORM
|
||||
assert result2["step_id"] == "manual"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.senseme.config_flow.async_get_device_by_ip_address",
|
||||
return_value=MOCK_DEVICE,
|
||||
), patch(
|
||||
"homeassistant.components.senseme.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_HOST: MOCK_ADDRESS,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result3["title"] == "Haiku Fan"
|
||||
assert result3["data"] == {
|
||||
"info": MOCK_DEVICE.get_device_info,
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_user_no_discovery(hass: HomeAssistant) -> None:
|
||||
"""Test we get the form as a user with no discovery."""
|
||||
|
||||
with _patch_discovery(no_device=True), patch(
|
||||
"homeassistant.components.senseme.config_flow.async_get_device_by_ip_address",
|
||||
return_value=MOCK_DEVICE,
|
||||
), patch(
|
||||
"homeassistant.components.senseme.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert not result["errors"]
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_HOST: "not a valid address",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == FlowResultType.FORM
|
||||
assert result2["step_id"] == "manual"
|
||||
assert result2["errors"] == {CONF_HOST: "invalid_host"}
|
||||
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{
|
||||
CONF_HOST: MOCK_ADDRESS,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result3["title"] == "Haiku Fan"
|
||||
assert result3["data"] == {
|
||||
"info": MOCK_DEVICE.get_device_info,
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_user_manual_entry_cannot_connect(hass: HomeAssistant) -> None:
|
||||
"""Test we get the form as a user."""
|
||||
|
||||
with _patch_discovery():
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert not result["errors"]
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"device": None,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == FlowResultType.FORM
|
||||
assert result2["step_id"] == "manual"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.senseme.config_flow.async_get_device_by_ip_address",
|
||||
return_value=None,
|
||||
):
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_HOST: MOCK_ADDRESS,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] == FlowResultType.FORM
|
||||
assert result3["step_id"] == "manual"
|
||||
assert result3["errors"] == {CONF_HOST: "cannot_connect"}
|
||||
|
||||
|
||||
async def test_discovery(hass: HomeAssistant) -> None:
|
||||
"""Test we can setup a discovered device."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"info": MOCK_DEVICE2.get_device_info,
|
||||
},
|
||||
unique_id=MOCK_DEVICE2.uuid,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with _patch_discovery(), patch(
|
||||
"homeassistant.components.senseme.async_get_device_by_device_info",
|
||||
return_value=(True, MOCK_DEVICE2),
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with _patch_discovery(), patch(
|
||||
"homeassistant.components.senseme.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={CONF_ID: MOCK_UUID},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert not result["errors"]
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"device": MOCK_UUID,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == "Haiku Fan"
|
||||
assert result2["data"] == {
|
||||
"info": MOCK_DEVICE.get_device_info,
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_discovery_existing_device_no_ip_change(hass: HomeAssistant) -> None:
|
||||
"""Test we can setup a discovered device."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"info": MOCK_DEVICE.get_device_info,
|
||||
},
|
||||
unique_id=MOCK_DEVICE.uuid,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with _patch_discovery(), patch(
|
||||
"homeassistant.components.senseme.async_get_device_by_device_info",
|
||||
return_value=(True, MOCK_DEVICE),
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with _patch_discovery():
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={CONF_ID: MOCK_UUID},
|
||||
)
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_discovery_existing_device_ip_change(hass: HomeAssistant) -> None:
|
||||
"""Test a config entry ips get updated from discovery."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"info": MOCK_DEVICE.get_device_info,
|
||||
},
|
||||
unique_id=MOCK_DEVICE.uuid,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with _patch_discovery(device=MOCK_DEVICE_ALTERNATE_IP), patch(
|
||||
"homeassistant.components.senseme.async_get_device_by_device_info",
|
||||
return_value=(True, MOCK_DEVICE),
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={CONF_ID: MOCK_UUID},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
assert entry.data["info"]["address"] == "127.0.0.8"
|
||||
|
||||
|
||||
async def test_dhcp_discovery_existing_config_entry(hass: HomeAssistant) -> None:
|
||||
"""Test dhcp discovery is aborted if there is an existing config entry."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"info": MOCK_DEVICE2.get_device_info,
|
||||
},
|
||||
unique_id=MOCK_DEVICE2.uuid,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY
|
||||
)
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_dhcp_discovery(hass: HomeAssistant) -> None:
|
||||
"""Test we can setup a dhcp discovered device."""
|
||||
with _patch_discovery(), patch(
|
||||
"homeassistant.components.senseme.config_flow.async_get_device_by_ip_address",
|
||||
return_value=MOCK_DEVICE,
|
||||
), patch(
|
||||
"homeassistant.components.senseme.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert not result["errors"]
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"device": MOCK_UUID,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == "Haiku Fan"
|
||||
assert result2["data"] == {
|
||||
"info": MOCK_DEVICE.get_device_info,
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_dhcp_discovery_cannot_connect(hass: HomeAssistant) -> None:
|
||||
"""Test we abort if we cannot cannot to a dhcp discovered device."""
|
||||
with _patch_discovery(), patch(
|
||||
"homeassistant.components.senseme.config_flow.async_get_device_by_ip_address",
|
||||
return_value=None,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY
|
||||
)
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "cannot_connect"
|
||||
|
||||
|
||||
async def test_dhcp_discovery_cannot_connect_no_uuid(hass: HomeAssistant) -> None:
|
||||
"""Test we abort if the discovered device has no uuid."""
|
||||
with _patch_discovery(), patch(
|
||||
"homeassistant.components.senseme.config_flow.async_get_device_by_ip_address",
|
||||
return_value=MOCK_DEVICE_NO_UUID,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY
|
||||
)
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "cannot_connect"
|
|
@ -1,115 +0,0 @@
|
|||
"""Tests for senseme light platform."""
|
||||
from aiosenseme import SensemeDevice
|
||||
|
||||
from homeassistant.components import senseme
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_MODE,
|
||||
ATTR_COLOR_TEMP,
|
||||
ATTR_SUPPORTED_COLOR_MODES,
|
||||
DOMAIN as LIGHT_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
ColorMode,
|
||||
)
|
||||
from homeassistant.components.senseme.const import DOMAIN
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import _mock_device, _patch_device, _patch_discovery
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def _setup_mocked_entry(hass: HomeAssistant, device: SensemeDevice) -> None:
|
||||
"""Set up a mocked entry."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={"info": device.get_device_info},
|
||||
unique_id=device.uuid,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
with _patch_discovery(), _patch_device(device=device):
|
||||
await async_setup_component(hass, senseme.DOMAIN, {senseme.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_light_unique_id(hass: HomeAssistant) -> None:
|
||||
"""Test a light unique id."""
|
||||
device = _mock_device()
|
||||
await _setup_mocked_entry(hass, device)
|
||||
entity_id = "light.haiku_fan"
|
||||
entity_registry = er.async_get(hass)
|
||||
assert entity_registry.async_get(entity_id).unique_id == f"{device.uuid}-LIGHT"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_ON
|
||||
|
||||
|
||||
async def test_fan_light(hass: HomeAssistant) -> None:
|
||||
"""Test a fan light."""
|
||||
device = _mock_device()
|
||||
await _setup_mocked_entry(hass, device)
|
||||
entity_id = "light.haiku_fan"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_ON
|
||||
attributes = state.attributes
|
||||
assert attributes[ATTR_BRIGHTNESS] == 255
|
||||
assert attributes[ATTR_COLOR_MODE] == ColorMode.BRIGHTNESS
|
||||
assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.BRIGHTNESS]
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
assert device.light_on is False
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
assert device.light_on is True
|
||||
|
||||
|
||||
async def test_fan_light_no_brightness(hass: HomeAssistant) -> None:
|
||||
"""Test a fan light without brightness."""
|
||||
device = _mock_device()
|
||||
device.brightness = None
|
||||
await _setup_mocked_entry(hass, device)
|
||||
entity_id = "light.haiku_fan"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_ON
|
||||
attributes = state.attributes
|
||||
assert attributes[ATTR_BRIGHTNESS] == 255
|
||||
assert attributes[ATTR_COLOR_MODE] == ColorMode.BRIGHTNESS
|
||||
assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.BRIGHTNESS]
|
||||
|
||||
|
||||
async def test_standalone_light(hass: HomeAssistant) -> None:
|
||||
"""Test a standalone light."""
|
||||
device = _mock_device()
|
||||
device.is_light = True
|
||||
device.light_color_temp_max = 6500
|
||||
device.light_color_temp_min = 2700
|
||||
device.light_color_temp = 4000
|
||||
await _setup_mocked_entry(hass, device)
|
||||
entity_id = "light.haiku_fan_light"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_ON
|
||||
attributes = state.attributes
|
||||
assert attributes[ATTR_BRIGHTNESS] == 255
|
||||
assert attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP
|
||||
assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.COLOR_TEMP]
|
||||
assert attributes[ATTR_COLOR_TEMP] == 250
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
assert device.light_on is False
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
assert device.light_on is True
|
Loading…
Reference in a new issue