diff --git a/custom_components/meteobridgesql/__init__.py b/custom_components/meteobridgesql/__init__.py new file mode 100644 index 0000000..b5bcd37 --- /dev/null +++ b/custom_components/meteobridgesql/__init__.py @@ -0,0 +1,127 @@ +"""Meteobridge SQL Platform.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from random import randrange +from types import MappingProxyType +from typing import Any, Self + +from pymeteobridgesql import ( + MeteobridgeSQLDatabaseConnectionError, + MeteobridgeSQLDataError, + MeteobridgeSQL, + RealtimeData, + StationData, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_MAC, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError, ConfigEntryNotReady, Unauthorized +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + CONF_DATABASE, + DOMAIN, +) + +PLATFORMS = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up MeteobridgeSQL as config entry.""" + + coordinator = MeteobridgeSQLDataUpdateCoordinator(hass, config_entry) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][config_entry.entry_id] = coordinator + + config_entry.async_on_unload(config_entry.add_update_listener(async_update_entry)) + + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload a config entry.""" + + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) + + hass.data[DOMAIN].pop(config_entry.entry_id) + + return unload_ok + +async def async_update_entry(hass: HomeAssistant, config_entry: ConfigEntry): + """Reload MeteobridgeSQL component when options changed.""" + await hass.config_entries.async_reload(config_entry.entry_id) + +class CannotConnect(HomeAssistantError): + """Unable to connect to the web site.""" + +class MeteobridgeSQLDataUpdateCoordinator(DataUpdateCoordinator["MeteobridgeSQLData"]): + """Class to manage fetching WeatherFlow data.""" + + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Initialize global MeteobridgeSQL data updater.""" + self.weather = MeteobridgeSQLData(hass, config_entry.data) + self.weather.initialize_data() + self.hass = hass + self.config_entry = config_entry + + update_interval = timedelta(minutes=1) + + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) + + + async def _async_update_data(self) -> MeteobridgeSQLData: + """Fetch data from MeteobridgeSQL.""" + try: + return await self.weather.fetch_data() + except Exception as err: + raise UpdateFailed(f"Update failed: {err}") from err + +class MeteobridgeSQLData: + """Keep data for MeteobridgeSQL entity data.""" + + def __init__(self, hass: HomeAssistant, config: MappingProxyType[str, Any]) -> None: + """Initialise the weather entity data.""" + self.hass = hass + self._config = config + self._weather_data: MeteobridgeSQL + self.sensor_data: RealtimeData + + def initialize_data(self) -> bool: + """Establish connection to API.""" + + self._weather_data = MeteobridgeSQL(self._config[CONF_HOST], self._config[CONF_USERNAME], self._config[CONF_PASSWORD], self._config[CONF_DATABASE], self._config[CONF_PORT]) + + return True + + async def fetch_data(self) -> Self: + """Fetch data from API - (current weather and forecast).""" + + try: + await self._weather_data.async_init() + self.sensor_data: RealtimeData = await self._weather_data.async_get_realtime_data(self._config[CONF_MAC]) + except MeteobridgeSQLDatabaseConnectionError as unauthorized: + _LOGGER.debug(unauthorized) + raise Unauthorized from unauthorized + except MeteobridgeSQLDataError as notreadyerror: + _LOGGER.debug(notreadyerror) + raise ConfigEntryNotReady from notreadyerror + + return self \ No newline at end of file diff --git a/custom_components/meteobridgesql/config_flow.py b/custom_components/meteobridgesql/config_flow.py index c6d7bb6..8eda522 100644 --- a/custom_components/meteobridgesql/config_flow.py +++ b/custom_components/meteobridgesql/config_flow.py @@ -14,7 +14,7 @@ ) from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from pymeteobridgesql import MeteobridgeSQL +from pymeteobridgesql import MeteobridgeSQL, MeteobridgeSQLDatabaseConnectionError from .const import (CONF_DATABASE, DEFAULT_PORT, DOMAIN) _LOGGER = logging.getLogger(__name__) @@ -38,8 +38,13 @@ async def async_step_user(self, user_input: dict[str, Any] | None = None) -> Flo errors = {} - meteobridge = MeteobridgeSQL(host=user_input[CONF_HOST],user=user_input[CONF_USERNAME],password=user_input[CONF_PASSWORD], database=user_input[CONF_DATABASE]) - meteobridge.async_init() + try: + meteobridge = MeteobridgeSQL(host=user_input[CONF_HOST],user=user_input[CONF_USERNAME],password=user_input[CONF_PASSWORD], database=user_input[CONF_DATABASE]) + await meteobridge.async_init() + except MeteobridgeSQLDatabaseConnectionError as error: + _LOGGER.error("Error connecting to MySQL Database: %s", error) + errors["base"] = "cannot_connect" + return await self._show_setup_form(errors) await self.async_set_unique_id(user_input[CONF_MAC]) self._abort_if_unique_id_configured @@ -71,6 +76,7 @@ async def _show_setup_form(self, errors=None): } ), errors=errors or {}, + ) class WeatherFlowForecastOptionsFlowHandler(config_entries.OptionsFlow): diff --git a/custom_components/meteobridgesql/const.py b/custom_components/meteobridgesql/const.py index 314a32e..ef2a6da 100644 --- a/custom_components/meteobridgesql/const.py +++ b/custom_components/meteobridgesql/const.py @@ -1,6 +1,16 @@ """Constants for Meteobridge SQL component.""" +ATTR_ATTRIBUTION= "Data provided by Meteobridge" +ATTR_MAX_SOLARRAD_TODAY = "max_solar_radiation_today" +ATTR_MAX_TEMP_TODAY = "max_temperature_today" +ATTR_MAX_UV_TODAY = "max_uv_today" +ATTR_MIN_TEMP_TODAY = "min_temperature_today" +ATTR_PRESSURE_TREND = "pressure_trend" +ATTR_TEMP_15_MIN = "temperature_15_min_ago" + CONF_DATABASE = "database" DEFAULT_PORT = 3306 DOMAIN = "meteobridgesql" + +MANUFACTURER = "Meteobridge" diff --git a/custom_components/meteobridgesql/manifest.json b/custom_components/meteobridgesql/manifest.json index f458f7e..39268e1 100644 --- a/custom_components/meteobridgesql/manifest.json +++ b/custom_components/meteobridgesql/manifest.json @@ -10,7 +10,7 @@ "iot_class": "cloud_polling", "issue_tracker": "https://github.com/briis/meteobridgesql/issues", "requirements": [ - "pymeteobridgesql==1.0.0" + "pymeteobridgesql==1.0.1" ], "version": "1.0.0" } \ No newline at end of file diff --git a/custom_components/meteobridgesql/sensor.py b/custom_components/meteobridgesql/sensor.py new file mode 100644 index 0000000..965859c --- /dev/null +++ b/custom_components/meteobridgesql/sensor.py @@ -0,0 +1,317 @@ +"""Support for MeteobridgeSQL sensor data.""" +from __future__ import annotations + +import logging + +from dataclasses import dataclass +from types import MappingProxyType +from typing import Any + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_MAC, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + DEGREE, + EntityCategory, + LIGHT_LUX, + PERCENTAGE, + UnitOfIrradiance, + UnitOfLength, + UnitOfPrecipitationDepth, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, + UnitOfTime, + UnitOfVolumetricFlux, + UnitOfElectricPotential, + UV_INDEX, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) +from homeassistant.util.dt import utc_from_timestamp +from homeassistant.util.unit_system import METRIC_SYSTEM + +from . import MeteobridgeSQLDataUpdateCoordinator +from .const import ( + ATTR_ATTRIBUTION, + ATTR_MAX_SOLARRAD_TODAY, + ATTR_MAX_TEMP_TODAY, + ATTR_MIN_TEMP_TODAY, + ATTR_MAX_UV_TODAY, + ATTR_PRESSURE_TREND, + ATTR_TEMP_15_MIN, + DOMAIN, + MANUFACTURER +) + +@dataclass +class MeteobridgeSQLEntityDescription(SensorEntityDescription): + """Describes MeteobridgeSQL sensor entity.""" + + +SENSOR_TYPES: tuple[MeteobridgeSQLEntityDescription, ...] = ( + MeteobridgeSQLEntityDescription( + key="beaufort", + name="Beaufort", + icon="mdi:windsock", + state_class=SensorStateClass.MEASUREMENT, + ), + MeteobridgeSQLEntityDescription( + key="dewpoint", + name="Dewpoint", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + MeteobridgeSQLEntityDescription( + key="feels_like_temperature", + name="Apparent Temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + MeteobridgeSQLEntityDescription( + key="heatindex", + name="Heat Index", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + MeteobridgeSQLEntityDescription( + key="humidity", + name="Humidity", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), + MeteobridgeSQLEntityDescription( + key="pm1", + name="Particulate Matter PM1", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM1, + state_class=SensorStateClass.MEASUREMENT, + ), + MeteobridgeSQLEntityDescription( + key="pm10", + name="Particulate Matter PM10", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM10, + state_class=SensorStateClass.MEASUREMENT, + ), + MeteobridgeSQLEntityDescription( + key="pm25", + name="Particulate Matter PM2.5", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM25, + state_class=SensorStateClass.MEASUREMENT, + ), + MeteobridgeSQLEntityDescription( + key="pressuretrend_text", + name="Pressure Trend", + translation_key="pressure_trend", + icon="mdi:trending-up", + ), + MeteobridgeSQLEntityDescription( + key="rainrate", + name="Rain rate", + native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, + device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + ), + MeteobridgeSQLEntityDescription( + key="raintoday", + name="Rain today", + native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS, + device_class=SensorDeviceClass.PRECIPITATION, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + ), + MeteobridgeSQLEntityDescription( + key="rainyesterday", + name="Rain yesterday", + native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS, + device_class=SensorDeviceClass.PRECIPITATION, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + ), + MeteobridgeSQLEntityDescription( + key="sealevelpressure", + name="Sealevel Pressure", + native_unit_of_measurement=UnitOfPressure.HPA, + device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + ), + MeteobridgeSQLEntityDescription( + key="solarrad", + name="Solar Radiation", + native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER, + device_class=SensorDeviceClass.IRRADIANCE, + state_class=SensorStateClass.MEASUREMENT, + ), + MeteobridgeSQLEntityDescription( + key="temperature", + name="Temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + MeteobridgeSQLEntityDescription( + key="uv", + name="UV Index", + native_unit_of_measurement=UV_INDEX, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:sun-wireless", + suggested_display_precision=1, + ), + MeteobridgeSQLEntityDescription( + key="windchill", + name="Wind Chill", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + MeteobridgeSQLEntityDescription( + key="windbearing", + name="Wind bearing", + native_unit_of_measurement=DEGREE, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:compass", + ), + MeteobridgeSQLEntityDescription( + key="wind_direction", + name="Wind Cardinal", + icon="mdi:compass", + translation_key="wind_cardinal", + ), + MeteobridgeSQLEntityDescription( + key="windspeedavg", + name="Wind Speed", + native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, + device_class=SensorDeviceClass.WIND_SPEED, + state_class=SensorStateClass.MEASUREMENT, + ), + MeteobridgeSQLEntityDescription( + key="windgust", + name="Wind Gust", + native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, + device_class=SensorDeviceClass.WIND_SPEED, + state_class=SensorStateClass.MEASUREMENT, + ), +) + + +_LOGGER = logging.getLogger(__name__) + +def _get_hw_platform(platform: str) -> str: + """Get Meteobridge hardware platform.""" + if platform == "CARAMBOLA2": + return "Meteobridge Pro" + if platform == "mbnano": + return "Meteobridge Nano" + return platform + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback) -> None: + """MeteobridgeSQL sensor platform.""" + coordinator: MeteobridgeSQLDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + if coordinator.data.sensor_data == {}: + return + + entities: list[MeteobridgeSQLSensor[Any]] = [ + MeteobridgeSQLSensor(coordinator, description, config_entry) + for description in SENSOR_TYPES if getattr(coordinator.data.sensor_data, description.key) is not None + ] + + async_add_entities(entities, False) + +class MeteobridgeSQLSensor(CoordinatorEntity[DataUpdateCoordinator], SensorEntity): + """A MeteobridgeSQL sensor.""" + + entity_description: MeteobridgeSQLEntityDescription + _attr_has_entity_name = True + + def __init__( + self, + coordinator: MeteobridgeSQLDataUpdateCoordinator, + description: MeteobridgeSQLEntityDescription, + config: MappingProxyType[str, Any] + ) -> None: + """Initialize a MeteobridgeSQL sensor.""" + super().__init__(coordinator) + self.entity_description = description + self._config = config + self._coordinator = coordinator + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._config.data[CONF_MAC])}, + entry_type=DeviceEntryType.SERVICE, + manufacturer=MANUFACTURER, + model=_get_hw_platform(self.coordinator.data.sensor_data.mb_platform), + name=f"{self.coordinator.data.sensor_data.mb_stationname} Sensors", + configuration_url=f"http://{self.coordinator.data.sensor_data.mb_ip}", + hw_version=f"{self.coordinator.data.sensor_data.mb_platform}", + sw_version=f"{self.coordinator.data.sensor_data.mb_swversion}-{self.coordinator.data.sensor_data.mb_buildnum}", + ) + self._attr_attribution = ATTR_ATTRIBUTION + self._attr_unique_id = f"{config.data[CONF_MAC]} {description.key}" + + @property + def native_unit_of_measurement(self) -> str | None: + """Return unit of sensor.""" + + return super().native_unit_of_measurement + + @property + def native_value(self) -> StateType: + """Return state of the sensor.""" + + return ( + getattr(self.coordinator.data.sensor_data, self.entity_description.key) + if self.coordinator.data.sensor_data else None + ) + + @property + def extra_state_attributes(self) -> None: + """Return non standard attributes.""" + + if self.entity_description.key == "temperature": + return { + ATTR_MAX_TEMP_TODAY: self.coordinator.data.sensor_data.tempmax, + ATTR_MIN_TEMP_TODAY: self.coordinator.data.sensor_data.tempmin, + ATTR_TEMP_15_MIN: self.coordinator.data.sensor_data.temp15min, + } + + if self.entity_description.key == "uv": + return { + ATTR_MAX_UV_TODAY: self.coordinator.data.sensor_data.uvdaymax, + } + + if self.entity_description.key == "solarrad": + return { + ATTR_MAX_SOLARRAD_TODAY: self.coordinator.data.sensor_data.solarraddaymax, + } + + if self.entity_description.key == "pressuretrend_text": + return { + ATTR_PRESSURE_TREND: self.coordinator.data.sensor_data.pressuretrend, + } + + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove( + self.coordinator.async_add_listener(self.async_write_ha_state) + ) diff --git a/custom_components/meteobridgesql/translations/en.json b/custom_components/meteobridgesql/translations/en.json index d2c4555..7adf0bb 100644 --- a/custom_components/meteobridgesql/translations/en.json +++ b/custom_components/meteobridgesql/translations/en.json @@ -4,11 +4,7 @@ "unique_id": "This Server has already been setup." }, "error": { - "wrong_station_id": "The Station ID entered is not correct. Check that you are not using Device ID.", - "server_error": "WeatherFlow servers encountered an unexpected error", - "wrong_token": "The API Token is incorrect or does not match the Station ID.", - "offline_error": "Station either is offline or no recent observations. Check station status or remove Sensors and try again.", - "bad_request": "An unknown error occurred when retrieving data." + "cannot_connect": "Cannot connect to the MySQL Database. Please check your input and try again" }, "step": { "user": { diff --git a/requirements.txt b/requirements.txt index 0a52e02..3f73061 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ homeassistant==2024.1.5 pip>=23.2.1,<23.4 ruff==0.1.14 ffmpeg==1.4 +mysql-connector-python==8.3.0