diff --git a/.coveragerc b/.coveragerc index c5c94aafbca..8c60a99f026 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1249,6 +1249,9 @@ omit = homeassistant/components/stookalert/__init__.py homeassistant/components/stookalert/binary_sensor.py homeassistant/components/stookalert/diagnostics.py + homeassistant/components/stookwijzer/__init__.py + homeassistant/components/stookwijzer/diagnostics.py + homeassistant/components/stookwijzer/sensor.py homeassistant/components/stream/* homeassistant/components/streamlabswater/* homeassistant/components/suez_water/* diff --git a/CODEOWNERS b/CODEOWNERS index 0e090732d4c..aa540e79d4e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1139,6 +1139,8 @@ build.json @home-assistant/supervisor /homeassistant/components/stiebel_eltron/ @fucm /homeassistant/components/stookalert/ @fwestenberg @frenck /tests/components/stookalert/ @fwestenberg @frenck +/homeassistant/components/stookwijzer/ @fwestenberg +/tests/components/stookwijzer/ @fwestenberg /homeassistant/components/stream/ @hunterjm @uvjustin @allenporter /tests/components/stream/ @hunterjm @uvjustin @allenporter /homeassistant/components/stt/ @pvizeli diff --git a/homeassistant/components/stookwijzer/__init__.py b/homeassistant/components/stookwijzer/__init__.py new file mode 100644 index 00000000000..d1950eaf0a3 --- /dev/null +++ b/homeassistant/components/stookwijzer/__init__.py @@ -0,0 +1,29 @@ +"""The Stookwijzer integration.""" +from __future__ import annotations + +from stookwijzer import Stookwijzer + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE, Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Stookwijzer from a config entry.""" + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = Stookwijzer( + entry.data[CONF_LOCATION][CONF_LATITUDE], + entry.data[CONF_LOCATION][CONF_LONGITUDE], + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Stookwijzer config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + del hass.data[DOMAIN][entry.entry_id] + return unload_ok diff --git a/homeassistant/components/stookwijzer/config_flow.py b/homeassistant/components/stookwijzer/config_flow.py new file mode 100644 index 00000000000..fdf4cc06f37 --- /dev/null +++ b/homeassistant/components/stookwijzer/config_flow.py @@ -0,0 +1,45 @@ +"""Config flow to configure the Stookwijzer integration.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.selector import LocationSelector + +from .const import DOMAIN + + +class StookwijzerFlowHandler(ConfigFlow, domain=DOMAIN): + """Config flow for Stookwijzer.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + + if user_input is not None: + return self.async_create_entry( + title="Stookwijzer", + data=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_LOCATION, + default={ + CONF_LATITUDE: self.hass.config.latitude, + CONF_LONGITUDE: self.hass.config.longitude, + }, + ): LocationSelector() + } + ), + ) diff --git a/homeassistant/components/stookwijzer/const.py b/homeassistant/components/stookwijzer/const.py new file mode 100644 index 00000000000..cdd5ac2a567 --- /dev/null +++ b/homeassistant/components/stookwijzer/const.py @@ -0,0 +1,16 @@ +"""Constants for the Stookwijzer integration.""" +import logging +from typing import Final + +from homeassistant.backports.enum import StrEnum + +DOMAIN: Final = "stookwijzer" +LOGGER = logging.getLogger(__package__) + + +class StookwijzerState(StrEnum): + """Stookwijzer states for sensor entity.""" + + BLUE = "blauw" + ORANGE = "oranje" + RED = "rood" diff --git a/homeassistant/components/stookwijzer/diagnostics.py b/homeassistant/components/stookwijzer/diagnostics.py new file mode 100644 index 00000000000..e29606cb191 --- /dev/null +++ b/homeassistant/components/stookwijzer/diagnostics.py @@ -0,0 +1,31 @@ +"""Diagnostics support for Stookwijzer.""" +from __future__ import annotations + +from typing import Any + +from stookwijzer import Stookwijzer + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + client: Stookwijzer = hass.data[DOMAIN][entry.entry_id] + + last_updated = None + if client.last_updated: + last_updated = client.last_updated.isoformat() + + return { + "state": client.state, + "last_updated": last_updated, + "lqi": client.lqi, + "windspeed": client.windspeed, + "weather": client.weather, + "concentrations": client.concentrations, + } diff --git a/homeassistant/components/stookwijzer/manifest.json b/homeassistant/components/stookwijzer/manifest.json new file mode 100644 index 00000000000..fc653fd6ebe --- /dev/null +++ b/homeassistant/components/stookwijzer/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "stookwijzer", + "name": "Stookwijzer", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/stookwijzer", + "codeowners": ["@fwestenberg"], + "requirements": ["stookwijzer==1.3.0"], + "integration_type": "service", + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/stookwijzer/sensor.py b/homeassistant/components/stookwijzer/sensor.py new file mode 100644 index 00000000000..9eb70fda7ee --- /dev/null +++ b/homeassistant/components/stookwijzer/sensor.py @@ -0,0 +1,65 @@ +"""This integration provides support for Stookwijzer Sensor.""" +from __future__ import annotations + +from datetime import timedelta + +from stookwijzer import Stookwijzer + +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, StookwijzerState + +SCAN_INTERVAL = timedelta(minutes=60) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Stookwijzer sensor from a config entry.""" + client = hass.data[DOMAIN][entry.entry_id] + async_add_entities([StookwijzerSensor(client, entry)], update_before_add=True) + + +class StookwijzerSensor(SensorEntity): + """Defines a Stookwijzer binary sensor.""" + + _attr_attribution = "Data provided by stookwijzer.nu" + _attr_device_class = SensorDeviceClass.ENUM + _attr_has_entity_name = True + _attr_translation_key = "stookwijzer" + + def __init__(self, client: Stookwijzer, entry: ConfigEntry) -> None: + """Initialize a Stookwijzer device.""" + self._client = client + self._attr_options = [cls.value for cls in StookwijzerState] + self._attr_unique_id = entry.entry_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"{entry.entry_id}")}, + name="Stookwijzer", + manufacturer="stookwijzer.nu", + entry_type=DeviceEntryType.SERVICE, + configuration_url="https://www.stookwijzer.nu", + ) + + def update(self) -> None: + """Update the data from the Stookwijzer handler.""" + self._client.update() + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self._client.state is not None + + @property + def native_value(self) -> str | None: + """Return the state of the device.""" + if self._client.state is None: + return None + return StookwijzerState(self._client.state).value diff --git a/homeassistant/components/stookwijzer/strings.json b/homeassistant/components/stookwijzer/strings.json new file mode 100644 index 00000000000..549673165ec --- /dev/null +++ b/homeassistant/components/stookwijzer/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "description": "Select the location you want to recieve the Stookwijzer information for.", + "data": { + "location": "[%key:common::config_flow::data::location%]" + } + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "Blue", + "oranje": "Orange", + "rood": "Red" + } + } + } + } +} diff --git a/homeassistant/components/stookwijzer/translations/en.json b/homeassistant/components/stookwijzer/translations/en.json new file mode 100644 index 00000000000..0aa8a093c69 --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "Location" + }, + "description": "Select the location you want to recieve the Stookwijzer information for." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "Blue", + "oranje": "Orange", + "rood": "Red" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index d55e1eee4f6..2e3c286629b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -411,6 +411,7 @@ FLOWS = { "steam_online", "steamist", "stookalert", + "stookwijzer", "subaru", "sun", "surepetcare", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index b57f2068626..68d022f43f8 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5243,6 +5243,12 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "stookwijzer": { + "name": "Stookwijzer", + "integration_type": "service", + "config_flow": true, + "iot_class": "cloud_polling" + }, "streamlabswater": { "name": "StreamLabs", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index e47acb71047..24c51570138 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2405,6 +2405,9 @@ steamodd==4.21 # homeassistant.components.stookalert stookalert==0.1.4 +# homeassistant.components.stookwijzer +stookwijzer==1.3.0 + # homeassistant.components.streamlabswater streamlabswater==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b6579a4c3e6..51bb69e3be5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1699,6 +1699,9 @@ steamodd==4.21 # homeassistant.components.stookalert stookalert==0.1.4 +# homeassistant.components.stookwijzer +stookwijzer==1.3.0 + # homeassistant.components.huawei_lte # homeassistant.components.solaredge # homeassistant.components.thermoworks_smoke diff --git a/tests/components/stookwijzer/__init__.py b/tests/components/stookwijzer/__init__.py new file mode 100644 index 00000000000..af7da13c8a6 --- /dev/null +++ b/tests/components/stookwijzer/__init__.py @@ -0,0 +1 @@ +"""Tests for the Stookwijzer integration.""" diff --git a/tests/components/stookwijzer/test_config_flow.py b/tests/components/stookwijzer/test_config_flow.py new file mode 100644 index 00000000000..b18eb54b322 --- /dev/null +++ b/tests/components/stookwijzer/test_config_flow.py @@ -0,0 +1,42 @@ +"""Tests for the Stookwijzer config flow.""" +from unittest.mock import patch + +from homeassistant.components.stookwijzer.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + + +async def test_full_user_flow(hass: HomeAssistant) -> None: + """Test the full user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + with patch( + "homeassistant.components.stookwijzer.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LOCATION: { + CONF_LATITUDE: 1.0, + CONF_LONGITUDE: 1.1, + } + }, + ) + + assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("data") == { + "location": { + "latitude": 1.0, + "longitude": 1.1, + }, + } + + assert len(mock_setup_entry.mock_calls) == 1