From d8337cf98f5b85d6acd582ddfe1edf3138ead568 Mon Sep 17 00:00:00 2001 From: stefano055415 Date: Wed, 30 Jun 2021 13:21:06 +0200 Subject: [PATCH] Add Freedompro (#46332) Co-authored-by: Milan Meulemans Co-authored-by: Maciej Bieniek Co-authored-by: Franck Nijhof --- CODEOWNERS | 1 + .../components/freedompro/__init__.py | 84 +++++++ .../components/freedompro/config_flow.py | 73 ++++++ homeassistant/components/freedompro/const.py | 3 + homeassistant/components/freedompro/light.py | 109 +++++++++ .../components/freedompro/manifest.json | 11 + .../components/freedompro/strings.json | 20 ++ .../freedompro/translations/en.json | 20 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/freedompro/__init__.py | 1 + tests/components/freedompro/conftest.py | 67 ++++++ tests/components/freedompro/const.py | 219 ++++++++++++++++++ .../components/freedompro/test_config_flow.py | 82 +++++++ tests/components/freedompro/test_init.py | 55 +++++ tests/components/freedompro/test_light.py | 155 +++++++++++++ 17 files changed, 907 insertions(+) create mode 100644 homeassistant/components/freedompro/__init__.py create mode 100644 homeassistant/components/freedompro/config_flow.py create mode 100644 homeassistant/components/freedompro/const.py create mode 100644 homeassistant/components/freedompro/light.py create mode 100644 homeassistant/components/freedompro/manifest.json create mode 100644 homeassistant/components/freedompro/strings.json create mode 100644 homeassistant/components/freedompro/translations/en.json create mode 100644 tests/components/freedompro/__init__.py create mode 100644 tests/components/freedompro/conftest.py create mode 100644 tests/components/freedompro/const.py create mode 100644 tests/components/freedompro/test_config_flow.py create mode 100644 tests/components/freedompro/test_init.py create mode 100644 tests/components/freedompro/test_light.py diff --git a/CODEOWNERS b/CODEOWNERS index 70fbaf8ce1f..46eb14dd66a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -169,6 +169,7 @@ homeassistant/components/forked_daapd/* @uvjustin homeassistant/components/fortios/* @kimfrellsen homeassistant/components/foscam/* @skgsergio homeassistant/components/freebox/* @hacf-fr @Quentame +homeassistant/components/freedompro/* @stefano055415 homeassistant/components/fritz/* @mammuth @AaronDavidSchneider @chemelli74 homeassistant/components/fritzbox/* @mib1185 homeassistant/components/fronius/* @nielstron diff --git a/homeassistant/components/freedompro/__init__.py b/homeassistant/components/freedompro/__init__.py new file mode 100644 index 00000000000..47c0bda1b1c --- /dev/null +++ b/homeassistant/components/freedompro/__init__.py @@ -0,0 +1,84 @@ +"""Support for freedompro.""" +from datetime import timedelta +import logging + +from pyfreedompro import get_list, get_states + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["light"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Freedompro from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + api_key = entry.data[CONF_API_KEY] + + coordinator = FreedomproDataUpdateCoordinator(hass, api_key) + await coordinator.async_config_entry_first_refresh() + + entry.async_on_unload(entry.add_update_listener(update_listener)) + + hass.data[DOMAIN][entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +async def update_listener(hass, config_entry): + """Update listener.""" + await hass.config_entries.async_reload(config_entry.entry_id) + + +class FreedomproDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching Freedompro data API.""" + + def __init__(self, hass, api_key): + """Initialize.""" + self._hass = hass + self._api_key = api_key + self._devices = None + + update_interval = timedelta(minutes=1) + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) + + async def _async_update_data(self): + if self._devices is None: + result = await get_list( + aiohttp_client.async_get_clientsession(self._hass), self._api_key + ) + if result["state"]: + self._devices = result["devices"] + else: + raise UpdateFailed() + + result = await get_states( + aiohttp_client.async_get_clientsession(self._hass), self._api_key + ) + + for device in self._devices: + dev = next( + (dev for dev in result if dev["uid"] == device["uid"]), + None, + ) + if dev is not None and "state" in dev: + device["state"] = dev["state"] + return self._devices diff --git a/homeassistant/components/freedompro/config_flow.py b/homeassistant/components/freedompro/config_flow.py new file mode 100644 index 00000000000..c1288e61406 --- /dev/null +++ b/homeassistant/components/freedompro/config_flow.py @@ -0,0 +1,73 @@ +"""Config flow to configure Freedompro.""" +from pyfreedompro import get_list +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_API_KEY +from homeassistant.helpers import aiohttp_client + +from .const import DOMAIN + +STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): str}) + + +class Hub: + """Freedompro Hub class.""" + + def __init__(self, hass, api_key): + """Freedompro Hub class init.""" + self._hass = hass + self._api_key = api_key + + async def authenticate(self): + """Freedompro Hub class authenticate.""" + return await get_list( + aiohttp_client.async_get_clientsession(self._hass), self._api_key + ) + + +async def validate_input(hass: core.HomeAssistant, api_key): + """Validate api key.""" + hub = Hub(hass, api_key) + result = await hub.authenticate() + if result["state"] is False: + if result["code"] == -201: + raise InvalidAuth + if result["code"] == -200: + raise CannotConnect + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Show the setup form to the user.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + errors = {} + + try: + await validate_input(self.hass, user_input[CONF_API_KEY]) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + else: + return self.async_create_entry(title="Freedompro", data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/freedompro/const.py b/homeassistant/components/freedompro/const.py new file mode 100644 index 00000000000..3f5df9283d4 --- /dev/null +++ b/homeassistant/components/freedompro/const.py @@ -0,0 +1,3 @@ +"""Constants for the Freedompro integration.""" + +DOMAIN = "freedompro" diff --git a/homeassistant/components/freedompro/light.py b/homeassistant/components/freedompro/light.py new file mode 100644 index 00000000000..ca96dba00f7 --- /dev/null +++ b/homeassistant/components/freedompro/light.py @@ -0,0 +1,109 @@ +"""Support for Freedompro light.""" +import json + +from pyfreedompro import put_state + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_HS_COLOR, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_HS, + COLOR_MODE_ONOFF, + LightEntity, +) +from homeassistant.const import CONF_API_KEY +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Freedompro light.""" + api_key = entry.data[CONF_API_KEY] + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + Device(hass, api_key, device, coordinator) + for device in coordinator.data + if device["type"] == "lightbulb" + ) + + +class Device(CoordinatorEntity, LightEntity): + """Representation of an Freedompro light.""" + + def __init__(self, hass, api_key, device, coordinator): + """Initialize the Freedompro light.""" + super().__init__(coordinator) + self._hass = hass + self._session = aiohttp_client.async_get_clientsession(self._hass) + self._api_key = api_key + self._attr_name = device["name"] + self._attr_unique_id = device["uid"] + self._type = device["type"] + self._characteristics = device["characteristics"] + self._attr_is_on = False + self._attr_brightness = 0 + color_mode = COLOR_MODE_ONOFF + if "hue" in self._characteristics: + color_mode = COLOR_MODE_HS + elif "brightness" in self._characteristics: + color_mode = COLOR_MODE_BRIGHTNESS + self._attr_color_mode = color_mode + self._attr_supported_color_modes = {color_mode} + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + device = next( + ( + device + for device in self.coordinator.data + if device["uid"] == self._attr_unique_id + ), + None, + ) + if device is not None and "state" in device: + state = device["state"] + if "on" in state: + self._attr_is_on = state["on"] + if "brightness" in state: + self._attr_brightness = round(state["brightness"] / 100 * 255) + if "hue" in state and "saturation" in state: + self._attr_hs_color = (state["hue"], state["saturation"]) + super()._handle_coordinator_update() + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self._handle_coordinator_update() + + async def async_turn_on(self, **kwargs): + """Async function to set on to light.""" + payload = {"on": True} + if ATTR_BRIGHTNESS in kwargs: + payload["brightness"] = round(kwargs[ATTR_BRIGHTNESS] / 255 * 100) + if ATTR_HS_COLOR in kwargs: + payload["saturation"] = round(kwargs[ATTR_HS_COLOR][1]) + payload["hue"] = round(kwargs[ATTR_HS_COLOR][0]) + payload = json.dumps(payload) + await put_state( + self._session, + self._api_key, + self._attr_unique_id, + payload, + ) + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs): + """Async function to set off to light.""" + payload = {"on": False} + payload = json.dumps(payload) + await put_state( + self._session, + self._api_key, + self._attr_unique_id, + payload, + ) + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/freedompro/manifest.json b/homeassistant/components/freedompro/manifest.json new file mode 100644 index 00000000000..94d57b37cae --- /dev/null +++ b/homeassistant/components/freedompro/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "freedompro", + "name": "Freedompro", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/freedompro", + "codeowners": [ + "@stefano055415" + ], + "requirements": ["pyfreedompro==1.1.0"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/freedompro/strings.json b/homeassistant/components/freedompro/strings.json new file mode 100644 index 00000000000..947a9bd2e33 --- /dev/null +++ b/homeassistant/components/freedompro/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + }, + "description": "Please enter the API key obtained from https://home.freedompro.eu", + "title": "Freedompro API key" + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/freedompro/translations/en.json b/homeassistant/components/freedompro/translations/en.json new file mode 100644 index 00000000000..c8952d56bfd --- /dev/null +++ b/homeassistant/components/freedompro/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" + }, + "step": { + "user": { + "data": { + "api_key": "API Key" + }, + "description": "Please enter the API key obtained from https://home.freedompro.eu", + "title": "Freedompro API key" + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 2f12ebd7d74..aa6d9009574 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -82,6 +82,7 @@ FLOWS = [ "forked_daapd", "foscam", "freebox", + "freedompro", "fritz", "fritzbox", "fritzbox_callmonitor", diff --git a/requirements_all.txt b/requirements_all.txt index 498e5154c21..eaa1283e750 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1450,6 +1450,9 @@ pyfnip==0.2 # homeassistant.components.forked_daapd pyforked-daapd==0.1.11 +# homeassistant.components.freedompro +pyfreedompro==1.1.0 + # homeassistant.components.fritzbox pyfritzhome==0.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 101280ddf3b..fb74d7cb53a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -809,6 +809,9 @@ pyflunearyou==1.0.7 # homeassistant.components.forked_daapd pyforked-daapd==0.1.11 +# homeassistant.components.freedompro +pyfreedompro==1.1.0 + # homeassistant.components.fritzbox pyfritzhome==0.4.2 diff --git a/tests/components/freedompro/__init__.py b/tests/components/freedompro/__init__.py new file mode 100644 index 00000000000..1f87c43b43c --- /dev/null +++ b/tests/components/freedompro/__init__.py @@ -0,0 +1 @@ +"""Tests for the Freedompro integration.""" diff --git a/tests/components/freedompro/conftest.py b/tests/components/freedompro/conftest.py new file mode 100644 index 00000000000..c43887fa487 --- /dev/null +++ b/tests/components/freedompro/conftest.py @@ -0,0 +1,67 @@ +"""Fixtures for Freedompro integration tests.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.freedompro.const import DOMAIN + +from tests.common import MockConfigEntry +from tests.components.freedompro.const import DEVICES, DEVICES_STATE + + +@pytest.fixture +async def init_integration(hass) -> MockConfigEntry: + """Set up the Freedompro integration in Home Assistant.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Feedompro", + unique_id="0123456", + data={ + "api_key": "gdhsksjdhcncjdkdjndjdkdmndjdjdkd", + }, + ) + + with patch( + "homeassistant.components.freedompro.get_list", + return_value={ + "state": True, + "devices": DEVICES, + }, + ), patch( + "homeassistant.components.freedompro.get_states", + return_value=DEVICES_STATE, + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry + + +@pytest.fixture +async def init_integration_no_state(hass) -> MockConfigEntry: + """Set up the Freedompro integration in Home Assistant without state.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Feedompro", + unique_id="0123456", + data={ + "api_key": "gdhsksjdhcncjdkdjndjdkdmndjdjdkd", + }, + ) + + with patch( + "homeassistant.components.freedompro.get_list", + return_value={ + "state": True, + "devices": DEVICES, + }, + ), patch( + "homeassistant.components.freedompro.get_states", + return_value=[], + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/freedompro/const.py b/tests/components/freedompro/const.py new file mode 100644 index 00000000000..8635858d000 --- /dev/null +++ b/tests/components/freedompro/const.py @@ -0,0 +1,219 @@ +"""Const Freedompro for test.""" + +DEVICES = [ + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*2VAS3HTWINNZ5N6HVEIPDJ6NX85P2-AM-GSYWUCNPU0", + "name": "Bathroom leak sensor", + "type": "leakSensor", + "characteristics": ["leakDetected"], + }, + { + "uid": "2WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*2VAS3HTWINNZ5N6HVEIPDJ6NX85P2-AM-GSYWUCNPU0", + "name": "lock", + "type": "lock", + "characteristics": ["lock"], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*ILYH1E3DWZOVMNEUIMDYMNLOW-LFRQFDPWWJOVHVDOS", + "name": "Bedroom fan", + "type": "fan", + "characteristics": ["on", "rotationSpeed"], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*SOT3NKALCRQMHUHJUF79NUG6UQP1IIQIN1PJVRRPT0C", + "name": "Contact sensor living room", + "type": "contactSensor", + "characteristics": ["contactSensorState"], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*VTEPEDYE8DXGS8U94CJKQDLKMN6CUX1IJWSOER2HZCK", + "name": "Doorway motion sensor", + "type": "motionSensor", + "characteristics": ["motionDetected"], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*QN-DDFMPEPRDOQV7W7JQG3NL0NPZGTLIBYT3HFSPNEY", + "name": "Garden humidity sensor", + "type": "humiditySensor", + "characteristics": ["currentRelativeHumidity"], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*1JKU1MVWHQL-Z9SCUS85VFXMRGNDCDNDDUVVDKBU31W", + "name": "Irrigation switch", + "type": "switch", + "characteristics": ["on"], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*JHJZIZ9ORJNHB7DZNBNAOSEDECVTTZ48SABTCA3WA3M", + "name": "lightbulb", + "type": "lightbulb", + "characteristics": ["on", "brightness", "saturation", "hue"], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*SNG7Y3R1R0S_W5BCNPP1O5WUN2NCEOOT27EFSYT6JYS", + "name": "Living room occupancy sensor", + "type": "occupancySensor", + "characteristics": ["occupancyDetected"], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*LWPVY7X1AX0DRWLYUUNZ3ZSTHMYNDDBQTPZCZQUUASA", + "name": "Living room temperature sensor", + "type": "temperatureSensor", + "characteristics": ["currentTemperature"], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*SXFMEXI4UMDBAMXXPI6LJV47O9NY-IRCAKZI7_MW0LY", + "name": "Smoke sensor kitchen", + "type": "smokeSensor", + "characteristics": ["smokeDetected"], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*R6V0FNNF7SACWZ8V9NCOX7UCYI4ODSYAOJWZ80PLJ3C", + "name": "Bedroom CO2 sensor", + "type": "carbonDioxideSensor", + "characteristics": ["carbonDioxideDetected", "carbonDioxideLevel"], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*3-QURR5Q6ADA8ML1TBRG59RRGM1F9LVUZLKPYKFJQHC", + "name": "bedroomlight", + "type": "lightbulb", + "characteristics": ["on"], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*TWMYQKL3UVED4HSIIB9GXJWJZBQCXG-9VE-N2IUAIWI", + "name": "Bedroom thermostat", + "type": "thermostat", + "characteristics": [ + "heatingCoolingState", + "currentTemperature", + "targetTemperature", + ], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*3XSSVIJWK-65HILWTC4WINQK46SP4OEZRCNO25VGWAS", + "name": "Bedroom window covering", + "type": "windowCovering", + "characteristics": ["position"], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*JVRAR_6WVL1Y0PJ5GFWGPMFV7FLVD4MZKBWXC_UFWYM", + "name": "Garden light sensors", + "type": "lightSensor", + "characteristics": ["currentAmbientLightLevel"], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*0PUTVZVJJJL-ZHZZBHTIBS3-J-U7JYNPACFPJW0MD-I", + "name": "Living room outlet", + "type": "outlet", + "characteristics": ["on"], + }, +] + +DEVICES_STATE = [ + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*2VAS3HTWINNZ5N6HVEIPDJ6NX85P2-AM-GSYWUCNPU0", + "type": "leakSensor", + "state": {"leakDetected": 0}, + "online": True, + }, + { + "uid": "2WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*2VAS3HTWINNZ5N6HVEIPDJ6NX85P2-AM-GSYWUCNPU0", + "type": "lock", + "state": {"lock": 0}, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*ILYH1E3DWZOVMNEUIMDYMNLOW-LFRQFDPWWJOVHVDOS", + "type": "fan", + "state": {"on": False, "rotationSpeed": 0}, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*SOT3NKALCRQMHUHJUF79NUG6UQP1IIQIN1PJVRRPT0C", + "type": "contactSensor", + "state": {"contactSensorState": True}, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*VTEPEDYE8DXGS8U94CJKQDLKMN6CUX1IJWSOER2HZCK", + "type": "motionSensor", + "state": {"motionDetected": False}, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*QN-DDFMPEPRDOQV7W7JQG3NL0NPZGTLIBYT3HFSPNEY", + "type": "humiditySensor", + "state": {"currentRelativeHumidity": 0}, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*1JKU1MVWHQL-Z9SCUS85VFXMRGNDCDNDDUVVDKBU31W", + "type": "switch", + "state": {"on": False}, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*JHJZIZ9ORJNHB7DZNBNAOSEDECVTTZ48SABTCA3WA3M", + "type": "lightbulb", + "state": {"on": True, "brightness": 0, "saturation": 0, "hue": 0}, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*SNG7Y3R1R0S_W5BCNPP1O5WUN2NCEOOT27EFSYT6JYS", + "type": "occupancySensor", + "state": {"occupancyDetected": False}, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*LWPVY7X1AX0DRWLYUUNZ3ZSTHMYNDDBQTPZCZQUUASA", + "type": "temperatureSensor", + "state": {"currentTemperature": 0}, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*SXFMEXI4UMDBAMXXPI6LJV47O9NY-IRCAKZI7_MW0LY", + "type": "smokeSensor", + "state": {"smokeDetected": False}, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*R6V0FNNF7SACWZ8V9NCOX7UCYI4ODSYAOJWZ80PLJ3C", + "type": "carbonDioxideSensor", + "state": {"carbonDioxideDetected": False, "carbonDioxideLevel": 0}, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*3-QURR5Q6ADA8ML1TBRG59RRGM1F9LVUZLKPYKFJQHC", + "type": "lightbulb", + "state": {"on": False}, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*TWMYQKL3UVED4HSIIB9GXJWJZBQCXG-9VE-N2IUAIWI", + "type": "thermostat", + "state": { + "heatingCoolingState": 1, + "currentTemperature": 14, + "targetTemperature": 14, + }, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*3XSSVIJWK-65HILWTC4WINQK46SP4OEZRCNO25VGWAS", + "type": "windowCovering", + "state": {"position": 0}, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*JVRAR_6WVL1Y0PJ5GFWGPMFV7FLVD4MZKBWXC_UFWYM", + "type": "lightSensor", + "state": {"currentAmbientLightLevel": 500}, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*0PUTVZVJJJL-ZHZZBHTIBS3-J-U7JYNPACFPJW0MD-I", + "type": "outlet", + "state": {"on": False}, + "online": True, + }, +] diff --git a/tests/components/freedompro/test_config_flow.py b/tests/components/freedompro/test_config_flow.py new file mode 100644 index 00000000000..f44cbd232ad --- /dev/null +++ b/tests/components/freedompro/test_config_flow.py @@ -0,0 +1,82 @@ +"""Define tests for the Freedompro config flow.""" +from unittest.mock import patch + +from homeassistant import data_entry_flow +from homeassistant.components.freedompro.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_API_KEY + +from tests.components.freedompro.const import DEVICES + +VALID_CONFIG = { + CONF_API_KEY: "ksdjfgslkjdfksjdfksjgfksjd", +} + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == SOURCE_USER + + +async def test_invalid_auth(hass): + """Test that errors are shown when API key is invalid.""" + with patch( + "homeassistant.components.freedompro.config_flow.list", + return_value={ + "state": False, + "code": -201, + }, + ): + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=VALID_CONFIG, + ) + + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_connection_error(hass): + """Test that errors are shown when API key is invalid.""" + with patch( + "homeassistant.components.freedompro.config_flow.get_list", + return_value={ + "state": False, + "code": -200, + }, + ): + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=VALID_CONFIG, + ) + + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_create_entry(hass): + """Test that the user step works.""" + with patch( + "homeassistant.components.freedompro.config_flow.get_list", + return_value={ + "state": True, + "devices": DEVICES, + }, + ): + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=VALID_CONFIG, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Freedompro" + assert result["data"][CONF_API_KEY] == "ksdjfgslkjdfksjdfksjgfksjd" diff --git a/tests/components/freedompro/test_init.py b/tests/components/freedompro/test_init.py new file mode 100644 index 00000000000..4e9d9ec197d --- /dev/null +++ b/tests/components/freedompro/test_init.py @@ -0,0 +1,55 @@ +"""Freedompro component tests.""" +import logging +from unittest.mock import patch + +from homeassistant.components.freedompro.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState + +from tests.common import MockConfigEntry + +LOGGER = logging.getLogger(__name__) + +ENTITY_ID = f"{DOMAIN}.fake_name" + + +async def test_async_setup_entry(hass, init_integration): + """Test a successful setup entry.""" + entry = init_integration + assert entry is not None + state = hass.states + assert state is not None + + +async def test_config_not_ready(hass): + """Test for setup failure if connection to Freedompro is missing.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Feedompro", + unique_id="0123456", + data={ + "api_key": "gdhsksjdhcncjdkdjndjdkdmndjdjdkd", + }, + ) + + with patch( + "homeassistant.components.freedompro.get_list", + return_value={ + "state": False, + }, + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_unload_entry(hass, init_integration): + """Test successful unload of entry.""" + entry = init_integration + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state == ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.NOT_LOADED diff --git a/tests/components/freedompro/test_light.py b/tests/components/freedompro/test_light.py new file mode 100644 index 00000000000..09a945ada03 --- /dev/null +++ b/tests/components/freedompro/test_light.py @@ -0,0 +1,155 @@ +"""Tests for the Freedompro light.""" +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_HS_COLOR, + DOMAIN as LIGHT_DOMAIN, + SERVICE_TURN_ON, +) +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, STATE_OFF, STATE_ON +from homeassistant.helpers import entity_registry as er + + +async def test_light_get_state(hass, init_integration): + """Test states of the light.""" + init_integration + registry = er.async_get(hass) + + entity_id = "light.lightbulb" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + assert state.attributes.get("friendly_name") == "lightbulb" + + entry = registry.async_get(entity_id) + assert entry + assert ( + entry.unique_id + == "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*JHJZIZ9ORJNHB7DZNBNAOSEDECVTTZ48SABTCA3WA3M" + ) + + +async def test_light_set_on(hass, init_integration): + """Test set on of the light.""" + init_integration + registry = er.async_get(hass) + + entity_id = "light.lightbulb" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + assert state.attributes.get("friendly_name") == "lightbulb" + + entry = registry.async_get(entity_id) + assert entry + assert ( + entry.unique_id + == "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*JHJZIZ9ORJNHB7DZNBNAOSEDECVTTZ48SABTCA3WA3M" + ) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + + +async def test_light_set_off(hass, init_integration): + """Test set off of the light.""" + init_integration + registry = er.async_get(hass) + + entity_id = "light.bedroomlight" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF + assert state.attributes.get("friendly_name") == "bedroomlight" + + entry = registry.async_get(entity_id) + assert entry + assert ( + entry.unique_id + == "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*3-QURR5Q6ADA8ML1TBRG59RRGM1F9LVUZLKPYKFJQHC" + ) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF + + +async def test_light_set_brightness(hass, init_integration): + """Test set brightness of the light.""" + init_integration + registry = er.async_get(hass) + + entity_id = "light.lightbulb" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + assert state.attributes.get("friendly_name") == "lightbulb" + + entry = registry.async_get(entity_id) + assert entry + assert ( + entry.unique_id + == "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*JHJZIZ9ORJNHB7DZNBNAOSEDECVTTZ48SABTCA3WA3M" + ) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: [entity_id], ATTR_BRIGHTNESS: 255}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + assert int(state.attributes[ATTR_BRIGHTNESS]) == 0 + + +async def test_light_set_hue(hass, init_integration): + """Test set brightness of the light.""" + init_integration + registry = er.async_get(hass) + + entity_id = "light.lightbulb" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + assert state.attributes.get("friendly_name") == "lightbulb" + + entry = registry.async_get(entity_id) + assert entry + assert ( + entry.unique_id + == "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*JHJZIZ9ORJNHB7DZNBNAOSEDECVTTZ48SABTCA3WA3M" + ) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: [entity_id], + ATTR_BRIGHTNESS: 255, + ATTR_HS_COLOR: (352.32, 100.0), + }, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + assert int(state.attributes[ATTR_BRIGHTNESS]) == 0 + assert state.attributes[ATTR_HS_COLOR] == (0, 0)