parent
5d2bac671f
commit
b0c7c72645
|
|
@ -0,0 +1,143 @@
|
|||
"""The Sencor SWS 12500 Weather Station integration."""
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
from aiohttp.web_exceptions import HTTPUnauthorized
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import InvalidStateError, PlatformNotReady
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import API_ID, API_KEY, DEFAULT_URL, DOMAIN, WINDY_ENABLED
|
||||
from .utils import remap_items
|
||||
from .windy_func import WindyPush
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
|
||||
class IncorrectDataError(InvalidStateError):
|
||||
"""Invalid exception."""
|
||||
|
||||
|
||||
class WeatherDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Manage fetched data."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: ConfigEntry) -> None:
|
||||
"""Init global updater."""
|
||||
self.hass = hass
|
||||
self.config = config
|
||||
self.windy = WindyPush(hass, config)
|
||||
super().__init__(hass, _LOGGER, name=DOMAIN)
|
||||
|
||||
async def recieved_data(self, webdata):
|
||||
"""Handle incoming data query."""
|
||||
data = webdata.query
|
||||
response = None
|
||||
|
||||
if "ID" not in data or "PASSWORD" not in data:
|
||||
_LOGGER.error("Invalid request. No security data provided!")
|
||||
raise HTTPUnauthorized
|
||||
|
||||
id_data = data["ID"]
|
||||
key_data = data["PASSWORD"]
|
||||
|
||||
_id = self.config_entry.options.get(API_ID)
|
||||
_key = self.config_entry.options.get(API_KEY)
|
||||
|
||||
if id_data != _id or key_data != _key:
|
||||
_LOGGER.error("Unauthorised access!")
|
||||
raise HTTPUnauthorized
|
||||
|
||||
if self.config_entry.options.get(WINDY_ENABLED):
|
||||
response = await self.windy.push_data_to_windy(data)
|
||||
|
||||
self.async_set_updated_data(remap_items(data))
|
||||
|
||||
response = response if response else "OK"
|
||||
|
||||
return aiohttp.web.Response(body=f"{response}", status=200)
|
||||
|
||||
|
||||
def register_path(
|
||||
hass: HomeAssistant, url_path: str, coordinator: WeatherDataUpdateCoordinator
|
||||
):
|
||||
"""Register path to handle incoming data."""
|
||||
try:
|
||||
route = hass.http.app.router.add_route(
|
||||
"GET", url_path, coordinator.recieved_data
|
||||
)
|
||||
except Exception: # pylint: disable=(broad-except)
|
||||
_LOGGER.error("Unable to register URL handler!")
|
||||
return False
|
||||
|
||||
_LOGGER.info(
|
||||
"Registered path to handle weather data: %s",
|
||||
route.get_info(), # pylint: disable=used-before-assignment
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def unregister_path():
|
||||
"""Unregister path to handle incoming data."""
|
||||
_LOGGER.error(
|
||||
"Unable to delete webhook from API! Restart HA before adding integration!"
|
||||
)
|
||||
|
||||
|
||||
class Weather(WeatherDataUpdateCoordinator):
|
||||
"""Weather class."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config) -> None:
|
||||
"""Init class."""
|
||||
self.hass = hass
|
||||
super().__init__(hass, config)
|
||||
|
||||
async def setup_update_listener(self, hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Update setup listener."""
|
||||
_LOGGER.info("Settings updated")
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up the config entry for my device."""
|
||||
|
||||
coordinator = WeatherDataUpdateCoordinator(hass, entry)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
|
||||
weather = Weather(hass, entry)
|
||||
|
||||
if not register_path(hass, DEFAULT_URL, coordinator):
|
||||
_LOGGER.error("Fatal: path not registered!")
|
||||
raise PlatformNotReady
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(weather.setup_update_listener))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
|
||||
_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if _ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
unregister_path()
|
||||
|
||||
return _ok
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the component.
|
||||
|
||||
This component can only be configured through the Integrations UI.
|
||||
"""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
return True
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
"""Config flow for Sencor SWS 12500 Weather Station integration."""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .const import (
|
||||
API_ID,
|
||||
API_KEY,
|
||||
DOMAIN,
|
||||
INVALID_CREDENTIALS,
|
||||
WINDY_API_KEY,
|
||||
WINDY_ENABLED,
|
||||
WINDY_LOGGER_ENABLED,
|
||||
)
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(API_ID, default="API ID"): str,
|
||||
vol.Required(API_KEY, default="API KEY"): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class CannotConnect(HomeAssistantError):
|
||||
"""We can not connect. - not used in push mechanism."""
|
||||
|
||||
|
||||
class InvalidAuth(HomeAssistantError):
|
||||
"""Invalid auth exception."""
|
||||
|
||||
|
||||
class ConfigOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Handle WeatherStation options."""
|
||||
|
||||
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Manage the options - show menu first."""
|
||||
return self.async_show_menu(step_id="init", menu_options=["basic", "windy"])
|
||||
|
||||
async def async_step_basic(self, user_input=None):
|
||||
"""Manage basic options - credentials."""
|
||||
errors = {}
|
||||
|
||||
api_id = self.config_entry.options.get(API_ID)
|
||||
api_key = self.config_entry.options.get(API_KEY)
|
||||
|
||||
OPTIONAL_USER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(API_ID, default=api_id): str,
|
||||
vol.Required(API_KEY, default=api_key): str,
|
||||
}
|
||||
)
|
||||
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="basic", data_schema=OPTIONAL_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
if user_input[API_ID] in INVALID_CREDENTIALS:
|
||||
errors["base"] = "valid_credentials_api"
|
||||
elif user_input[API_KEY] in INVALID_CREDENTIALS:
|
||||
errors["base"] = "valid_credentials_key"
|
||||
elif user_input[API_KEY] == user_input[API_ID]:
|
||||
errors["base"] = "valid_credentials_match"
|
||||
else:
|
||||
# retain Windy options
|
||||
data: dict = {}
|
||||
data[WINDY_API_KEY] = self.config_entry.options.get(WINDY_API_KEY)
|
||||
data[WINDY_ENABLED] = self.config_entry.options.get(WINDY_ENABLED)
|
||||
data[WINDY_LOGGER_ENABLED] = self.config_entry.options.get(
|
||||
WINDY_LOGGER_ENABLED
|
||||
)
|
||||
|
||||
data.update(user_input)
|
||||
|
||||
return self.async_create_entry(title=DOMAIN, data=data)
|
||||
|
||||
# we are ending with error msg, reshow form
|
||||
return self.async_show_form(
|
||||
step_id="basic", data_schema=OPTIONAL_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_windy(self, user_input=None):
|
||||
"""Manage windy options."""
|
||||
errors = {}
|
||||
|
||||
windy_key = self.config_entry.options.get(WINDY_API_KEY)
|
||||
windy_enabled = self.config_entry.options.get(WINDY_ENABLED)
|
||||
windy_logger_enabled = self.config_entry.options.get(WINDY_LOGGER_ENABLED)
|
||||
|
||||
OPTIONAL_USER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Optional(WINDY_API_KEY, default=windy_key): str,
|
||||
vol.Optional(WINDY_ENABLED, default=windy_enabled): bool,
|
||||
vol.Optional(WINDY_LOGGER_ENABLED, default=windy_logger_enabled): bool,
|
||||
}
|
||||
)
|
||||
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="windy", data_schema=OPTIONAL_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
if (user_input[WINDY_ENABLED] is True) and (user_input[WINDY_API_KEY] == ""):
|
||||
errors["base"] = "windy_key_required"
|
||||
return self.async_show_form(
|
||||
step_id="windy", data_schema=OPTIONAL_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
# retain API_ID and API_KEY from config
|
||||
data: dict = {}
|
||||
data[API_ID] = self.config_entry.options.get(API_ID)
|
||||
data[API_KEY] = self.config_entry.options.get(API_KEY)
|
||||
|
||||
data.update(user_input)
|
||||
|
||||
return self.async_create_entry(title=DOMAIN, data=data)
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Sencor SWS 12500 Weather Station."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step."""
|
||||
if user_input is None:
|
||||
await self.async_set_unique_id(DOMAIN)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
|
||||
)
|
||||
|
||||
errors = {}
|
||||
|
||||
if user_input[API_ID] in INVALID_CREDENTIALS:
|
||||
errors["base"] = "valid_credentials_api"
|
||||
elif user_input[API_KEY] in INVALID_CREDENTIALS:
|
||||
errors["base"] = "valid_credentials_key"
|
||||
elif user_input[API_KEY] == user_input[API_ID]:
|
||||
errors["base"] = "valid_credentials_match"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=DOMAIN, data=user_input, options=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: config_entries.ConfigEntry,
|
||||
) -> ConfigOptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return ConfigOptionsFlowHandler(config_entry)
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
"""Constants."""
|
||||
|
||||
from typing import Final
|
||||
|
||||
DOMAIN = "sws12500"
|
||||
DEFAULT_URL = "/weatherstation/updateweatherstation.php"
|
||||
WINDY_URL = "https://stations.windy.com/pws/update/"
|
||||
|
||||
ICON = "mdi:weather"
|
||||
|
||||
API_KEY = "API_KEY"
|
||||
API_ID = "API_ID"
|
||||
WINDY_API_KEY = "WINDY_API_KEY"
|
||||
WINDY_ENABLED: Final = "windy_enabled_checkbox"
|
||||
WINDY_LOGGER_ENABLED: Final = "windy_logger_checkbox"
|
||||
|
||||
WINDY_NOT_INSERTED: Final = "Data was succefuly sent to Windy, but not inserted by Windy API. Does anyone else sent data to Windy?"
|
||||
WINDY_INVALID_KEY: Final = "Windy API KEY is invalid. Send data to Windy is now disabled. Check your API KEY and try again."
|
||||
WINDY_SUCCESS: Final = (
|
||||
"Windy successfully sent data and data was successfully inserted by Windy API"
|
||||
)
|
||||
WINDY_UNEXPECTED: Final = (
|
||||
"Windy responded unexpectedly 3 times in a row. Send to Windy is now disabled!"
|
||||
)
|
||||
|
||||
INVALID_CREDENTIALS: Final = [
|
||||
"API",
|
||||
"API_ID",
|
||||
"API ID",
|
||||
"_ID",
|
||||
"ID",
|
||||
"API KEY",
|
||||
"API_KEY",
|
||||
"KEY",
|
||||
"_KEY",
|
||||
]
|
||||
|
||||
PURGE_DATA: Final = [
|
||||
"ID",
|
||||
"PASSWORD",
|
||||
"action",
|
||||
"rtfreq",
|
||||
"realtime",
|
||||
"dateutc",
|
||||
"solarradiation",
|
||||
"indoortempf",
|
||||
"indoorhumidity",
|
||||
"dailyrainin",
|
||||
]
|
||||
|
||||
BARO_PRESSURE: Final = "baro_pressure"
|
||||
OUTSIDE_TEMP: Final = "outside_temp"
|
||||
DEW_POINT: Final = "dew_point"
|
||||
OUTSIDE_HUMIDITY: Final = "outside_humidity"
|
||||
WIND_SPEED: Final = "wind_speed"
|
||||
WIND_GUST: Final = "wind_gust"
|
||||
WIND_DIR: Final = "wind_dir"
|
||||
RAIN: Final = "rain"
|
||||
DAILY_RAIN: Final = "daily_rain"
|
||||
SOLAR_RADIATION: Final = "solar_radiation"
|
||||
INDOOR_TEMP: Final = "indoor_temp"
|
||||
INDOOR_HUMIDITY: Final = "indoor_humidity"
|
||||
UV: Final = "uv"
|
||||
CH2_TEMP: Final = "ch2_temp"
|
||||
CH2_HUMIDITY: Final = "ch2_humidity"
|
||||
|
||||
|
||||
REMAP_ITEMS: dict = {
|
||||
"baromin": BARO_PRESSURE,
|
||||
"tempf": OUTSIDE_TEMP,
|
||||
"dewptf": DEW_POINT,
|
||||
"humidity": OUTSIDE_HUMIDITY,
|
||||
"windspeedmph": WIND_SPEED,
|
||||
"windgustmph": WIND_GUST,
|
||||
"winddir": WIND_DIR,
|
||||
"rainin": RAIN,
|
||||
"dailyrainin": DAILY_RAIN,
|
||||
"solarradiation": SOLAR_RADIATION,
|
||||
"indoortempf": INDOOR_TEMP,
|
||||
"indoorhumidity": INDOOR_HUMIDITY,
|
||||
"UV": UV,
|
||||
"soiltempf": CH2_TEMP,
|
||||
"soilmoisture": CH2_HUMIDITY,
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"domain": "sws12500",
|
||||
"name": "Sencor SWS 12500 Weather Station",
|
||||
"codeowners": ["@schizza"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["http"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/sws12500",
|
||||
"homekit": {},
|
||||
"iot_class": "local_push",
|
||||
"requirements": [],
|
||||
"ssdp": [],
|
||||
"version": "0.0.2",
|
||||
"zeroconf": []
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
@property
|
||||
def translation_key(self):
|
||||
"""Return translation key."""
|
||||
return self.entity_description.translation_key
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return device class."""
|
||||
return self.entity_description.device_class
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the switch."""
|
||||
return str(self.entity_description.name)
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique, Home Assistant friendly identifier for this entity."""
|
||||
return self.entity_description.key
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return value of entity."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""Return icon of entity."""
|
||||
return str(self.entity_description.icon)
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str:
|
||||
"""Return unit of measurement."""
|
||||
return str(self.entity_description.native_unit_of_measurement)
|
||||
|
||||
@property
|
||||
def state_class(self) -> str:
|
||||
"""Return stateClass."""
|
||||
|
||||
return str(self.entity_description.state_class)
|
||||
|
||||
@property
|
||||
def suggested_unit_of_measurement(self) -> str:
|
||||
"""Return sugestet_unit_of_measurement."""
|
||||
return str(self.entity_description.suggested_unit_of_measurement)
|
||||
|
||||
|
|
@ -0,0 +1,262 @@
|
|||
"""Sensors definition for SWS12500."""
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntityDescription,
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
UnitOfIrradiance,
|
||||
UnitOfPrecipitationDepth,
|
||||
UnitOfPressure,
|
||||
UnitOfSpeed,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import WeatherDataUpdateCoordinator
|
||||
from .const import (
|
||||
BARO_PRESSURE,
|
||||
DAILY_RAIN,
|
||||
DEW_POINT,
|
||||
DOMAIN,
|
||||
INDOOR_HUMIDITY,
|
||||
INDOOR_TEMP,
|
||||
OUTSIDE_HUMIDITY,
|
||||
OUTSIDE_TEMP,
|
||||
RAIN,
|
||||
SOLAR_RADIATION,
|
||||
UV,
|
||||
WIND_DIR,
|
||||
WIND_GUST,
|
||||
WIND_SPEED,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class WeatherSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describe Weather Sensor entities."""
|
||||
|
||||
|
||||
SENSOR_TYPES: tuple[WeatherSensorEntityDescription, ...] = (
|
||||
WeatherSensorEntityDescription(
|
||||
key=INDOOR_TEMP,
|
||||
name="Indoor temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
icon="mdi:thermometer",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
),
|
||||
WeatherSensorEntityDescription(
|
||||
key=INDOOR_HUMIDITY,
|
||||
name="Indoor humidity",
|
||||
native_unit_of_measurement="%",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
icon="mdi:thermometer",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
),
|
||||
WeatherSensorEntityDescription(
|
||||
key=OUTSIDE_TEMP,
|
||||
name="Outside Temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
icon="mdi:thermometer",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
),
|
||||
WeatherSensorEntityDescription(
|
||||
key=OUTSIDE_HUMIDITY,
|
||||
name="Outside humidity",
|
||||
native_unit_of_measurement="%",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
icon="mdi:thermometer",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
),
|
||||
WeatherSensorEntityDescription(
|
||||
key=DEW_POINT,
|
||||
name="Dew point",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
icon="mdi:thermometer-lines",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
),
|
||||
WeatherSensorEntityDescription(
|
||||
key=BARO_PRESSURE,
|
||||
name="Barometric pressure",
|
||||
native_unit_of_measurement=UnitOfPressure.INHG,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
icon="mdi:thermometer-lines",
|
||||
device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
|
||||
suggested_unit_of_measurement=UnitOfPressure.HPA,
|
||||
),
|
||||
WeatherSensorEntityDescription(
|
||||
key=WIND_SPEED,
|
||||
name="Wind speed",
|
||||
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
suggested_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||
icon="mdi:weather-windy",
|
||||
),
|
||||
WeatherSensorEntityDescription(
|
||||
key=WIND_GUST,
|
||||
name="Wind gust",
|
||||
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
suggested_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||
icon="mdi:windsock",
|
||||
),
|
||||
WeatherSensorEntityDescription(
|
||||
key=WIND_DIR,
|
||||
name="Wind direction",
|
||||
native_unit_of_measurement="°",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
icon="mdi:sign-direction",
|
||||
),
|
||||
WeatherSensorEntityDescription(
|
||||
key=RAIN,
|
||||
name="Rain",
|
||||
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
|
||||
device_class=SensorDeviceClass.PRECIPITATION,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
suggested_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
|
||||
suggested_display_precision=2,
|
||||
icon="mdi:weather-pouring",
|
||||
),
|
||||
WeatherSensorEntityDescription(
|
||||
key=DAILY_RAIN,
|
||||
name="Daily precipitation",
|
||||
native_unit_of_measurement="in/d",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
|
||||
suggested_unit_of_measurement="mm/h",
|
||||
suggested_display_precision=2,
|
||||
icon="mdi:weather-pouring",
|
||||
),
|
||||
WeatherSensorEntityDescription(
|
||||
key=SOLAR_RADIATION,
|
||||
name="Solar irradiance",
|
||||
native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.IRRADIANCE,
|
||||
icon="mdi:weather-sunny",
|
||||
),
|
||||
WeatherSensorEntityDescription(
|
||||
key=UV,
|
||||
name="UV index",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement="",
|
||||
icon="mdi:sunglasses",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info=None,
|
||||
) -> None:
|
||||
"""Set up Weather Station sensors."""
|
||||
|
||||
coordinator: WeatherDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
sensors = []
|
||||
for description in SENSOR_TYPES:
|
||||
sensors.append(WeatherSensor(hass, description, coordinator))
|
||||
async_add_entities(sensors)
|
||||
|
||||
|
||||
class WeatherSensor(CoordinatorEntity[WeatherDataUpdateCoordinator], SensorEntity):
|
||||
"""Implementation of Weather Sensor entity."""
|
||||
|
||||
entity_description: WeatherSensorEntityDescription
|
||||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
description: WeatherSensorEntityDescription,
|
||||
coordinator: WeatherDataUpdateCoordinator,
|
||||
) -> None:
|
||||
"""Initialize sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.hass = hass
|
||||
self.coordinator = coordinator
|
||||
self.entity_description = description
|
||||
self._state: StateType = None
|
||||
self._attrs: dict[str, Any] = {}
|
||||
self._available = False
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._state = self.coordinator.data.get(self.entity_description.key)
|
||||
|
||||
self.async_write_ha_state()
|
||||
self.async_registry_entry_updated()
|
||||
|
||||
@property
|
||||
def device_class(self) -> SensorDeviceClass:
|
||||
"""Return device class."""
|
||||
return self.entity_description.device_class
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the switch."""
|
||||
return str(self.entity_description.name)
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique, Home Assistant friendly identifier for this entity."""
|
||||
return self.entity_description.key
|
||||
|
||||
@property
|
||||
def native_value(self) -> None:
|
||||
"""Return value of entity."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""Return icon of entity."""
|
||||
return str(self.entity_description.icon)
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str:
|
||||
"""Return unit of measurement."""
|
||||
return str(self.entity_description.native_unit_of_measurement)
|
||||
|
||||
@property
|
||||
def state_class(self) -> str:
|
||||
"""Return stateClass."""
|
||||
|
||||
return str(self.entity_description.state_class)
|
||||
|
||||
@property
|
||||
def suggested_unit_of_measurement(self) -> str:
|
||||
"""Return sugestet_unit_of_measurement."""
|
||||
return str(self.entity_description.suggested_unit_of_measurement)
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Device info."""
|
||||
return DeviceInfo(
|
||||
connections=set(),
|
||||
name="Weather Station SWS 12500",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN,)}, # type: ignore[arg-type]
|
||||
manufacturer="Schizza",
|
||||
model="Weather Station SWS 12500",
|
||||
)
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Configure access for Weather Station",
|
||||
"description": "Provide API ID and API KEY so the Weather Station can access HomeAssistant",
|
||||
"data": {
|
||||
"api_id": "API ID / Station ID",
|
||||
"api_key": "API KEY / Password"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"valid_credentials_api": "Provide valid API ID.",
|
||||
"valid_credentials_key": "Provide valid API KEY.",
|
||||
"valid_credentials_match": "API ID and API KEY should not be the same."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Configure SWS12500 Integration",
|
||||
"description": "Choose what do you want to configure. If basic access or resending data for Windy site",
|
||||
"menu_options": {
|
||||
"basic": "Basic - configure credentials for Weather Station",
|
||||
"windy": "Windy configuration"
|
||||
}
|
||||
},
|
||||
"basic": {
|
||||
"title": "Configure credentials",
|
||||
"description": "Provide API ID and API KEY so the Weather Station can access HomeAssistant",
|
||||
"data": {
|
||||
"API_ID": "API ID / Station ID",
|
||||
"API_KEY": "API KEY / Password"
|
||||
}
|
||||
},
|
||||
"windy": {
|
||||
"title": "Configure Windy",
|
||||
"description": "Resend weather data to your Windy stations.",
|
||||
"data": {
|
||||
"WINDY_API_KEY": "API KEY provided by Windy",
|
||||
"windy_enabled_checkbox": "Enable resending data to Windy",
|
||||
"windy_logger_checkbox": "Log Windy data and responses"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"valid_credentials_api": "Provide valid API ID.",
|
||||
"valid_credentials_key": "Provide valid API KEY.",
|
||||
"valid_credentials_match": "API ID and API KEY should not be the same.",
|
||||
"windy_key_required": "Windy API key is required if you want to enable this function."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"valid_credentials_api": "Provide valid API ID.",
|
||||
"valid_credentials_key": "Provide valid API KEY.",
|
||||
"valid_credentials_match": "API ID and API KEY should not be the same."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_id": "API ID / Station ID",
|
||||
"api_key": "API KEY / Password"
|
||||
},
|
||||
"description": "Provide API ID and API KEY so the Weather Station can access HomeAssistant",
|
||||
"title": "Configure access for Weather Station"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"error": {
|
||||
"valid_credentials_api": "Provide valid API ID.",
|
||||
"valid_credentials_key": "Provide valid API KEY.",
|
||||
"valid_credentials_match": "API ID and API KEY should not be the same.",
|
||||
"windy_key_required": "Windy API key is required if you want to enable this function."
|
||||
},
|
||||
"step": {
|
||||
"basic": {
|
||||
"data": {
|
||||
"API_ID": "API ID / Station ID",
|
||||
"API_KEY": "API KEY / Password"
|
||||
},
|
||||
"description": "Provide API ID and API KEY so the Weather Station can access HomeAssistant",
|
||||
"title": "Configure credentials"
|
||||
},
|
||||
"init": {
|
||||
"description": "Choose what do you want to configure. If basic access or resending data for Windy site",
|
||||
"menu_options": {
|
||||
"basic": "Basic - configure credentials for Weather Station",
|
||||
"windy": "Windy configuration"
|
||||
},
|
||||
"title": "Configure SWS12500 Integration"
|
||||
},
|
||||
"windy": {
|
||||
"data": {
|
||||
"WINDY_API_KEY": "API KEY provided by Windy",
|
||||
"windy_enabled_checkbox": "Enable resending data to Windy",
|
||||
"windy_logger_checkbox": "Log Windy data and responses"
|
||||
},
|
||||
"description": "Resend weather data to your Windy stations.",
|
||||
"title": "Configure Windy"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
"""Utils for SWS12500."""
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import REMAP_ITEMS
|
||||
|
||||
|
||||
def update_options(
|
||||
hass: HomeAssistant, entry: ConfigEntry, update_key, update_value
|
||||
) -> None:
|
||||
"""Update config.options entry."""
|
||||
conf = {}
|
||||
|
||||
for k in entry.options:
|
||||
conf[k] = entry.options[k]
|
||||
|
||||
conf[update_key] = update_value
|
||||
|
||||
hass.config_entries.async_update_entry(entry, options=conf)
|
||||
|
||||
|
||||
def remap_items(entities):
|
||||
"""Remap items in query."""
|
||||
items = {}
|
||||
for item in entities:
|
||||
if item in REMAP_ITEMS:
|
||||
items[REMAP_ITEMS[item]] = entities[item]
|
||||
|
||||
return items
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
"""Windy functions."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import (
|
||||
PURGE_DATA,
|
||||
WINDY_API_KEY,
|
||||
WINDY_ENABLED,
|
||||
WINDY_INVALID_KEY,
|
||||
WINDY_LOGGER_ENABLED,
|
||||
WINDY_NOT_INSERTED,
|
||||
WINDY_SUCCESS,
|
||||
WINDY_UNEXPECTED,
|
||||
WINDY_URL,
|
||||
)
|
||||
from .utils import update_options
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
RESPONSE_FOR_TEST = False
|
||||
|
||||
|
||||
class WindyNotInserted(Exception):
|
||||
"""NotInserted state."""
|
||||
|
||||
|
||||
class WindySuccess(Exception):
|
||||
"""WindySucces state."""
|
||||
|
||||
|
||||
class WindyApiKeyError(Exception):
|
||||
"""Windy API Key error."""
|
||||
|
||||
|
||||
def timed(minutes: int):
|
||||
"""Simulate timedelta.
|
||||
|
||||
So we can mock td in tests.
|
||||
"""
|
||||
return timedelta(minutes=minutes)
|
||||
|
||||
|
||||
class WindyPush:
|
||||
"""Push data to Windy."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: ConfigEntry) -> None:
|
||||
"""Init."""
|
||||
self.hass = hass
|
||||
self.config = config
|
||||
|
||||
""" lets wait for 1 minute to get initial data from station
|
||||
and then try to push first data to Windy
|
||||
"""
|
||||
self.last_update = datetime.now()
|
||||
self.next_update = datetime.now() + timed(minutes=1)
|
||||
|
||||
self.log = self.config.options.get(WINDY_LOGGER_ENABLED)
|
||||
self.invalid_response_count = 0
|
||||
|
||||
def verify_windy_response( # pylint: disable=useless-return
|
||||
self,
|
||||
response: str,
|
||||
) -> WindyNotInserted | WindySuccess | WindyApiKeyError | None:
|
||||
"""Verify answer form Windy."""
|
||||
|
||||
if "NOTICE" in response:
|
||||
raise WindyNotInserted
|
||||
|
||||
if "SUCCESS" in response:
|
||||
raise WindySuccess
|
||||
|
||||
if "Invalid API key" in response:
|
||||
raise WindyApiKeyError
|
||||
|
||||
return None
|
||||
|
||||
async def push_data_to_windy(self, data):
|
||||
"""Pushes weather data do Windy stations.
|
||||
|
||||
Interval is 5 minutes, otherwise Windy would not accepts data.
|
||||
|
||||
we are sending almost the same data as we received
|
||||
from station. But we need to do some clean up.
|
||||
"""
|
||||
|
||||
text_for_test = None
|
||||
|
||||
if self.log:
|
||||
_LOGGER.info(
|
||||
"Windy last update = %s, next update at: %s",
|
||||
str(self.last_update),
|
||||
str(self.next_update),
|
||||
)
|
||||
|
||||
if self.next_update > datetime.now():
|
||||
return False
|
||||
|
||||
purged_data = dict(data)
|
||||
|
||||
for purge in PURGE_DATA:
|
||||
if purge in purged_data:
|
||||
purged_data.pop(purge)
|
||||
|
||||
if "dewptf" in purged_data:
|
||||
dewpoint = round(((float(purged_data.pop("dewptf")) - 32) / 1.8), 1)
|
||||
purged_data["dewpoint"] = str(dewpoint)
|
||||
|
||||
windy_api_key = self.config.options.get(WINDY_API_KEY)
|
||||
request_url = f"{WINDY_URL}{windy_api_key}"
|
||||
|
||||
if self.log:
|
||||
_LOGGER.info("Dataset for windy: %s", purged_data)
|
||||
|
||||
async with aiohttp.ClientSession(
|
||||
connector=aiohttp.TCPConnector(ssl=False), trust_env=True
|
||||
) as session: # verify_ssl=False; intended to be False
|
||||
try:
|
||||
async with session.get(request_url, params=purged_data) as resp:
|
||||
status = await resp.text()
|
||||
try:
|
||||
self.verify_windy_response(status)
|
||||
except WindyNotInserted:
|
||||
# log despite of settings
|
||||
_LOGGER.error(WINDY_NOT_INSERTED)
|
||||
|
||||
text_for_test = WINDY_NOT_INSERTED
|
||||
|
||||
except WindyApiKeyError:
|
||||
# log despite of settings
|
||||
_LOGGER.critical(WINDY_INVALID_KEY)
|
||||
text_for_test = WINDY_INVALID_KEY
|
||||
|
||||
update_options(self.hass, self.config, WINDY_ENABLED, False)
|
||||
|
||||
except WindySuccess:
|
||||
if self.log:
|
||||
_LOGGER.info(WINDY_SUCCESS)
|
||||
text_for_test = WINDY_SUCCESS
|
||||
|
||||
except aiohttp.ClientConnectionError as ex:
|
||||
_LOGGER.critical("Invalid response from Windy: %s", str(ex))
|
||||
self.invalid_response_count += 1
|
||||
if self.invalid_response_count > 3:
|
||||
_LOGGER.critical(WINDY_UNEXPECTED)
|
||||
text_for_test = WINDY_UNEXPECTED
|
||||
update_options(self.hass, self.config, WINDY_ENABLED, False)
|
||||
|
||||
self.last_update = datetime.now()
|
||||
self.next_update = self.last_update + timed(minutes=5)
|
||||
|
||||
if self.log:
|
||||
_LOGGER.info("Next update: %s", str(self.next_update))
|
||||
if RESPONSE_FOR_TEST and text_for_test:
|
||||
return text_for_test
|
||||
return None
|
||||
Loading…
Reference in New Issue