diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..496ee2c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md new file mode 100755 index 0000000..3237a71 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +![Version](https://img.shields.io/github/v/release/MarcoGos/davis_vantage?include_prereleases) + +# Davis Vantage + +This is a custom integration for the Davis Vantage Pro2 and Vue. + +## Installation + +Via HACS: + +- Add the following custom repository as an integration: + - MarcoGos/davis_vantage +- Restart Home Assistant +- Add the integration to Home Assistant + +## Setup + +During the setup of the integration the serial port of the weather station needs to be provided. + +Examples: +- tcp:192.168.0.18:1111 +- serial:/dev/ttyUSB0:19200:8N1 + +![Setup](/assets/setup.png) + +## What to expect + +The following sensors will be registered: + +![Sensors](/assets/sensors.png) + +The sensor information is updated every 30 seconds. diff --git a/custom_components/davis_vantage/__init__.py b/custom_components/davis_vantage/__init__.py new file mode 100755 index 0000000..a42e026 --- /dev/null +++ b/custom_components/davis_vantage/__init__.py @@ -0,0 +1,69 @@ +"""The Davis Vantage integration.""" +from __future__ import annotations +from typing import Any +import logging + +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.const import Platform + +from .client import DavisVantageClient +from .const import DOMAIN, NAME, VERSION, MANUFACTURER +from .coordinator import DavisVantageDataUpdateCoordinator + +PLATFORMS: list[Platform] = [ + Platform.SENSOR, + Platform.BINARY_SENSOR +] + +_LOGGER: logging.Logger = logging.getLogger(__package__) + +async def async_setup(hass: HomeAssistant, config: Any) -> bool: + return True + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Davis Vantage from a config entry.""" + if hass.data.get(DOMAIN) is None: + hass.data.setdefault(DOMAIN, {}) + + _LOGGER.debug(f"entry.data: {entry.data}") + + protocol = entry.data.get("protocol", "") + link = entry.data.get("link", "") + rain_collector = entry.data.get("rain_collector", "0.01""") + windrose8 = entry.data.get("windrose8", False) + + client = DavisVantageClient(hass, protocol, link, rain_collector, windrose8) + + device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, entry.entry_id)}, + manufacturer=MANUFACTURER, + name=NAME, + sw_version=VERSION + ) + + hass.data[DOMAIN][entry.entry_id] = coordinator = DavisVantageDataUpdateCoordinator( + hass=hass, client=client, device_info=device_info) + + await coordinator.async_config_entry_first_refresh() + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(async_reload_entry)) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unloaded := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + return unloaded + + +async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Reload config entry.""" + await async_unload_entry(hass, entry) + await async_setup_entry(hass, entry) diff --git a/custom_components/davis_vantage/assets/icon.png b/custom_components/davis_vantage/assets/icon.png new file mode 100755 index 0000000..12ac776 Binary files /dev/null and b/custom_components/davis_vantage/assets/icon.png differ diff --git a/custom_components/davis_vantage/assets/icon@2x.png b/custom_components/davis_vantage/assets/icon@2x.png new file mode 100755 index 0000000..f362c81 Binary files /dev/null and b/custom_components/davis_vantage/assets/icon@2x.png differ diff --git a/custom_components/davis_vantage/assets/logo.png b/custom_components/davis_vantage/assets/logo.png new file mode 100755 index 0000000..798c227 Binary files /dev/null and b/custom_components/davis_vantage/assets/logo.png differ diff --git a/custom_components/davis_vantage/assets/logo2.png b/custom_components/davis_vantage/assets/logo2.png new file mode 100755 index 0000000..c60ddfb Binary files /dev/null and b/custom_components/davis_vantage/assets/logo2.png differ diff --git a/custom_components/davis_vantage/binary_sensor.py b/custom_components/davis_vantage/binary_sensor.py new file mode 100755 index 0000000..03edab4 --- /dev/null +++ b/custom_components/davis_vantage/binary_sensor.py @@ -0,0 +1,73 @@ +from homeassistant.components.binary_sensor import ( + DOMAIN as BINARY_SENSOR_DOMAIN, + BinarySensorEntity, + BinarySensorEntityDescription +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DEFAULT_NAME, DOMAIN +from .coordinator import DavisVantageDataUpdateCoordinator + +DESCRIPTIONS: list[BinarySensorEntityDescription] = [ + BinarySensorEntityDescription( + key="IsRaining", + name="Is Raining" + ) +] + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Davis Vantage sensors based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + entities: list[DavisVantageBinarySensor] = [] + + # Add all binary sensors described above. + for description in DESCRIPTIONS: + entities.append( + DavisVantageBinarySensor( + coordinator=coordinator, + entry_id=entry.entry_id, + description=description, + ) + ) + + async_add_entities(entities) + + +class DavisVantageBinarySensor(CoordinatorEntity[DavisVantageDataUpdateCoordinator], BinarySensorEntity): + """Defines a Davis Vantage sensor.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: DavisVantageDataUpdateCoordinator, + entry_id: str, + description: BinarySensorEntityDescription, + ) -> None: + """Initialize Davis Vantage sensor.""" + super().__init__(coordinator=coordinator) + + self.entity_id = ( + f"{BINARY_SENSOR_DOMAIN}.{DEFAULT_NAME}_{description.name}".lower() + ) + self.entity_description = description + self._attr_name = description.name # type: ignore + self._attr_unique_id = f"{entry_id}-{DEFAULT_NAME} {self.name}" + self._attr_device_info = coordinator.device_info + + @property + def is_on(self) -> bool | None: + """Return the is_on of the sensor.""" + key = self.entity_description.key + data = self.coordinator.data + if key not in data: + return None + return data.get(key, False) # type: ignore \ No newline at end of file diff --git a/custom_components/davis_vantage/client.py b/custom_components/davis_vantage/client.py new file mode 100755 index 0000000..79257b5 --- /dev/null +++ b/custom_components/davis_vantage/client.py @@ -0,0 +1,95 @@ +from typing import Any +from datetime import datetime +import logging +from pyvantagepro import VantagePro2 +from pyvantagepro.parser import LoopDataParserRevB + +from homeassistant.core import HomeAssistant + +from .utils import * +from .const import RAIN_COLLECTOR_METRIC, PROTOCOL_NETWORK, PROTOCOL_SERIAL + +TIMEOUT = 10 + +_LOGGER: logging.Logger = logging.getLogger(__package__) + +class DavisVantageClient: + def __init__( + self, + hass: HomeAssistant, + protocol: str, + link: str, + rain_collector: str, + windrose8: bool + ) -> None: + self._hass = hass + self._protocol = protocol + self._link = link + self._windrose8 = windrose8 + self._rain_collector = rain_collector + self._last_data = LoopDataParserRevB + + async def async_get_current_data(self) -> LoopDataParserRevB | None: + """Get current date from weather station.""" + data = self._last_data + try: + vantagepro2 = VantagePro2.from_url(self.get_link()) # type: ignore + # vantagepro2.link.open() # type: ignore + data = vantagepro2.get_current_data() + if data: + self.add_additional_info(data) + self.convert_values(data) + data['LastError'] = "No error" + self._last_data = data + else: + data['LastError'] = "Couldn't acquire data" + except Exception as e: + _LOGGER.warning(f"Couldn't acquire data from {self._link}") + data['LastError'] = f"Couldn't acquire data: {e}" # type: ignore + # finally: + # vantagepro2.link.close() # type: ignore + return data # type: ignore + + async def async_get_davis_time(self) -> datetime | None: + """Get time from weather station.""" + data = None + try: + vantagepro2 = VantagePro2.from_url(self.get_link()) # type: ignore + # vantagepro2.link.open() # type: ignore + data = vantagepro2.gettime() + except Exception: + _LOGGER.warning(f"Couldn't acquire data from {self._link}") + # finally: + # vantagepro2.link.close() # type: ignore + return data + + def add_additional_info(self, data: dict[str, Any]) -> None: + data['HeatIndex'] = calc_heat_index(data['TempOut'], data['HumOut']) + data['WindChill'] = calc_wind_chill(data['TempOut'], data['WindSpeed']) + data['FeelsLike'] = calc_feels_like(data['TempOut'], data['HumOut'], data['WindSpeed']) + data['WindDirRose'] = get_wind_rose(data['WindDir'], self._windrose8) + data['DewPoint'] = calc_dew_point(data['TempOut'], data['HumOut']) + data['WindSpeedBft'] = convert_kmh_to_bft(convert_to_kmh(data['WindSpeed10Min'])) + data['IsRaining'] = data['RainRate'] > 0 + + def convert_values(self, data: dict[str, Any]) -> None: + data['Datetime'] = convert_to_iso_datetime(data['Datetime'], ZoneInfo(self._hass.config.time_zone)) + data['BarTrend'] = get_baro_trend(data['BarTrend']) + data['UV'] = get_uv(data['UV']) + data['ForecastRuleNo'] = get_forecast_string(data['ForecastRuleNo']) + data['RainCollector'] = self._rain_collector + data['WindRoseSetup'] = 8 if self._windrose8 else 16 + if data['RainCollector'] == RAIN_COLLECTOR_METRIC: + self.correct_rain_values(data) + + def correct_rain_values(self, data: dict[str, Any]): + data['RainDay'] *= 2/2.54 + data['RainMonth'] *= 2/2.54 + data['RainYear'] *= 2/2.54 + data['RainRate'] *= 2/2.54 + + def get_link(self) -> str | None: + if self._protocol == PROTOCOL_NETWORK: + return f"tcp:{self._link}" + if self._protocol == PROTOCOL_SERIAL: + return f"serial:{self._link}:19200:8N1" diff --git a/custom_components/davis_vantage/config_flow.py b/custom_components/davis_vantage/config_flow.py new file mode 100755 index 0000000..588dfc5 --- /dev/null +++ b/custom_components/davis_vantage/config_flow.py @@ -0,0 +1,161 @@ +"""Config flow for Davis Vantage integration.""" +from __future__ import annotations + +import logging +from typing import Any +import voluptuous as vol +import serial +import serial.tools.list_ports + +from homeassistant import config_entries +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.selector import selector #type: ignore + +from .const import ( + NAME, + DOMAIN, + DEFAULT_SYNC_INTERVAL, + RAIN_COLLECTOR_IMPERIAL, + RAIN_COLLECTOR_METRIC, + PROTOCOL_NETWORK, + PROTOCOL_SERIAL +) +from .client import DavisVantageClient + +_LOGGER = logging.getLogger(__name__) + + +class PlaceholderHub: + """Placeholder class to make tests pass.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize.""" + self._hass = hass + + async def authenticate(self, protocol: str, link: str) -> bool: + """Test if we can find data for the given link.""" + _LOGGER.info(f"authenticate called") + client = DavisVantageClient(self._hass, protocol, link, '', False) + return await client.async_get_davis_time() != None + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: + """Validate the user input allows us to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + hub = PlaceholderHub(hass) + if not await hub.authenticate(data["protocol"], data["link"]): + raise InvalidAuth + + # Return info that you want to store in the config entry. + return {"title": NAME} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Davis Vantage.""" + + VERSION = 1 + protocol: str + link: str + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is not None: + self.protocol = user_input['protocol'] + if self.protocol == PROTOCOL_SERIAL: + return await self.async_step_setup_serial() + + return await self.async_step_setup_network() + + list_of_types = [PROTOCOL_SERIAL, PROTOCOL_NETWORK] + schema = vol.Schema({vol.Required('protocol'): vol.In(list_of_types)}) + return self.async_show_form(step_id="user", data_schema=schema) + + async def async_step_setup_serial( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + + if user_input is not None: + self.link = user_input['link'] + return await self.async_step_setup_other_info() + + ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports) + list_of_ports = { + port.device: f"{port}, s/n: {port.serial_number or 'n/a'}" + + (f" - {port.manufacturer}" if port.manufacturer else "") + for port in ports + } + + STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required("link"): vol.In(list_of_ports) + } + ) + + return self.async_show_form( + step_id="setup_serial", data_schema=STEP_USER_DATA_SCHEMA + ) + + async def async_step_setup_network( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + if user_input is not None: + self.link = user_input['link'] + return await self.async_step_setup_other_info() + + STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required("link"): str + } + ) + + return self.async_show_form( + step_id="setup_network", data_schema=STEP_USER_DATA_SCHEMA + ) + + async def async_step_setup_other_info( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + + errors = {} + if user_input is not None: + await self.async_set_unique_id(DOMAIN) + self._abort_if_unique_id_configured() + user_input['protocol'] = self.protocol + user_input['link'] = self.link + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry(title=info["title"], data=user_input) + + list_of_rain_collector = [RAIN_COLLECTOR_IMPERIAL, RAIN_COLLECTOR_METRIC] + STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required("interval", default=DEFAULT_SYNC_INTERVAL): int, #type: ignore + vol.Required("rain_collector"): vol.In(list_of_rain_collector), + vol.Required("windrose8"): bool + } + ) + + return self.async_show_form( + step_id="setup_other_info", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/custom_components/davis_vantage/const.py b/custom_components/davis_vantage/const.py new file mode 100755 index 0000000..9457458 --- /dev/null +++ b/custom_components/davis_vantage/const.py @@ -0,0 +1,16 @@ +"""Constants for the Davis Vantage integration.""" + +NAME = "Davis Vantage" +DOMAIN = "davis_vantage" +MANUFACTURER = "Davis" +MODEL = "Vantage Pro2/Vue" +VERSION = "0.0.1" + +DEFAULT_SYNC_INTERVAL = 30 # seconds +DEFAULT_NAME = NAME + +RAIN_COLLECTOR_IMPERIAL = '0.01"' +RAIN_COLLECTOR_METRIC = '0.2 mm' + +PROTOCOL_NETWORK = 'Netwerk' +PROTOCOL_SERIAL = 'Serial' \ No newline at end of file diff --git a/custom_components/davis_vantage/coordinator.py b/custom_components/davis_vantage/coordinator.py new file mode 100755 index 0000000..5cfdf2f --- /dev/null +++ b/custom_components/davis_vantage/coordinator.py @@ -0,0 +1,43 @@ +from datetime import timedelta +from typing import Any +import logging + +from homeassistant.helpers.update_coordinator import UpdateFailed, DataUpdateCoordinator +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.core import HomeAssistant +from pyvantagepro.parser import LoopDataParserRevB + +from .client import DavisVantageClient +from .const import ( + DEFAULT_SYNC_INTERVAL, + DOMAIN, +) + +_LOGGER: logging.Logger = logging.getLogger(__package__) + +class DavisVantageDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching data from the weather station.""" + data: LoopDataParserRevB + + def __init__(self, hass: HomeAssistant, client: DavisVantageClient, device_info: DeviceInfo) -> None: + """Initialize.""" + self.client: DavisVantageClient = client + self.platforms: list[str] = [] + self.last_updated = None + self.device_info = device_info + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=DEFAULT_SYNC_INTERVAL), + ) + + async def _async_update_data(self) -> dict[str, Any]: + """Update data via library.""" + try: + data: LoopDataParserRevB = await self.client.async_get_current_data() # type: ignore + return data + except Exception as exception: + _LOGGER.error(f"Error DavisVantageDataUpdateCoordinator _async_update_data: {exception}") + raise UpdateFailed() from exception diff --git a/custom_components/davis_vantage/manifest.json b/custom_components/davis_vantage/manifest.json new file mode 100755 index 0000000..085629e --- /dev/null +++ b/custom_components/davis_vantage/manifest.json @@ -0,0 +1,17 @@ +{ + "domain": "davis_vantage", + "name": "Davis Vantage", + "version": "0.0.1", + "config_flow": true, + "documentation": "https://github.com/MarcoGos/davis_vantage", + "requirements": ["PyVantagePro==0.3.2"], + "ssdp": [], + "zeroconf": [], + "homekit": {}, + "dependencies": [], + "codeowners": [ + "@MarcoGos" + ], + "iot_class": "", + "integration_type": "hub" +} diff --git a/custom_components/davis_vantage/sensor.py b/custom_components/davis_vantage/sensor.py new file mode 100755 index 0000000..e4bb910 --- /dev/null +++ b/custom_components/davis_vantage/sensor.py @@ -0,0 +1,349 @@ +from homeassistant.components.sensor import ( + DOMAIN as SENSOR_DOMAIN, + SensorEntity, + SensorEntityDescription, + SensorDeviceClass +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, DEGREE, UnitOfSpeed, UnitOfLength, UnitOfVolumetricFlux, UnitOfElectricPotential, UnitOfTemperature, UnitOfPressure +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DEFAULT_NAME, DOMAIN +from .coordinator import DavisVantageDataUpdateCoordinator + +DESCRIPTIONS: list[SensorEntityDescription] = [ + SensorEntityDescription( + key="Datetime", + name="Davis Time", + icon="mdi:clock", + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC + ), + SensorEntityDescription( + key="TempOut", + name="Temperature (Outside)", + device_class=SensorDeviceClass.TEMPERATURE, + state_class="measurement", + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + suggested_display_precision=1 + ), + SensorEntityDescription( + key="TempIn", + name="Temperature (Inside)", + device_class=SensorDeviceClass.TEMPERATURE, + state_class="measurement", + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + suggested_display_precision=1 + ), + SensorEntityDescription( + key="HeatIndex", + name="Heat Index", + icon="mdi:sun-thermometer-outline", + device_class=SensorDeviceClass.TEMPERATURE, + state_class="measurement", + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + suggested_display_precision=1 + ), + SensorEntityDescription( + key="WindChill", + name="Wind Chill", + icon="mdi:snowflake-thermometer", + device_class=SensorDeviceClass.TEMPERATURE, + state_class="measurement", + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + suggested_display_precision=1 + ), + SensorEntityDescription( + key="FeelsLike", + name="Feels Like", + icon="mdi:download-circle-outline", + device_class=SensorDeviceClass.TEMPERATURE, + state_class="measurement", + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + suggested_display_precision=1 + ), + SensorEntityDescription( + key="DewPoint", + name="Dew Point", + icon="mdi:water-thermometer-outline", + device_class=SensorDeviceClass.TEMPERATURE, + state_class="measurement", + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + suggested_display_precision=1 + ), + SensorEntityDescription( + key="Barometer", + name="Barometric Pressure", + device_class=SensorDeviceClass.PRESSURE, + state_class="measurement", + native_unit_of_measurement=UnitOfPressure.INHG, + suggested_display_precision=2 + ), + SensorEntityDescription( + key="BarTrend", + name="Barometric Trend" + ), + SensorEntityDescription( + key="HumIn", + name="Humidity (Inside)", + device_class=SensorDeviceClass.HUMIDITY, + state_class="measurement", + native_unit_of_measurement=PERCENTAGE, + suggested_display_precision=0 + ), + SensorEntityDescription( + key="HumOut", + name="Humidity (Outside)", + device_class=SensorDeviceClass.HUMIDITY, + state_class="measurement", + native_unit_of_measurement=PERCENTAGE, + suggested_display_precision=0 + ), + SensorEntityDescription( + key="WindSpeed", + name="Wind Speed", + icon="mdi:weather-windy", + device_class=SensorDeviceClass.WIND_SPEED, + state_class="measurement", + native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, + suggested_display_precision=1 + ), + SensorEntityDescription( + key="WindSpeed10Min", + name="Wind Speed (Average)", + icon="mdi:weather-windy", + device_class=SensorDeviceClass.WIND_SPEED, + state_class="measurement", + native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, + suggested_display_precision=1 + ), + SensorEntityDescription( + key="WindDir", + name="Wind Direction", + icon="mdi:compass-outline", + state_class="measurement", + native_unit_of_measurement=DEGREE, + suggested_display_precision=0 + ), + SensorEntityDescription( + key="WindDirRose", + name="Wind Direction Rose", + icon="mdi:compass-outline" + ), + SensorEntityDescription( + key="WindSpeedBft", + name="Wind Speed (Bft)", + icon="mdi:weather-windy", + state_class="measurement", + entity_registry_enabled_default=False + ), + SensorEntityDescription( + key="RainDay", + name="Rain (Day)", + icon="mdi:water", + device_class=SensorDeviceClass.PRECIPITATION, + state_class="total_increasing", + native_unit_of_measurement=UnitOfLength.INCHES, + suggested_display_precision=1 + ), + SensorEntityDescription( + key="RainMonth", + name="Rain (Month)", + icon="mdi:water", + device_class=SensorDeviceClass.PRECIPITATION, + native_unit_of_measurement=UnitOfLength.INCHES, + suggested_display_precision=1 + ), + SensorEntityDescription( + key="RainYear", + name="Rain (Year)", + icon="mdi:water", + device_class=SensorDeviceClass.PRECIPITATION, + native_unit_of_measurement=UnitOfLength.INCHES, + suggested_display_precision=1 + ), + SensorEntityDescription( + key="RainRate", + name="Rain Rate", + icon="mdi:water", + device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, + state_class="measurement", + native_unit_of_measurement=UnitOfVolumetricFlux.INCHES_PER_HOUR, + suggested_display_precision=1 + ), + SensorEntityDescription( + key="UV", + name="UV Level", + icon="mdi:sun-wireless-outline", + state_class="measurement", + entity_registry_enabled_default=False + ), + SensorEntityDescription( + key="SolarRad", + name="Solar Radiation", + icon="mdi:sun-wireless-outline", + state_class="measurement", + entity_registry_enabled_default=False + ), + SensorEntityDescription( + key="BatteryVolts", + name="Battery Voltage", + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + entity_category=EntityCategory.DIAGNOSTIC, + suggested_display_precision=1 + ), + SensorEntityDescription( + key="ForecastIcon", + name="Forecast Icon", + entity_registry_enabled_default=False + ), + SensorEntityDescription( + key="ForecastRuleNo", + name="Forecast Rule", + icon="mdi:binoculars" + ), + SensorEntityDescription( + key="LastError", + name="Last Error", + icon="mdi:message-alert-outline", + entity_category=EntityCategory.DIAGNOSTIC + ), + SensorEntityDescription( + key="RainCollector", + name="Rain Collector", + icon="mdi:bucket-outline", + entity_category=EntityCategory.DIAGNOSTIC + ), + SensorEntityDescription( + key="WindRoseSetup", + name="Cardinal Directions", + icon="mdi:compass-rose", + entity_category=EntityCategory.DIAGNOSTIC + ), + SensorEntityDescription( + key="ExtraTemps01", + name="Extra Temperature 1", + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class="measurement", + entity_registry_enabled_default=False, + suggested_display_precision=1 + ), + SensorEntityDescription( + key="ExtraTemps02", + name="Extra Temperature 2", + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class="measurement", + entity_registry_enabled_default=False, + suggested_display_precision=1 + ), + SensorEntityDescription( + key="ExtraTemps03", + name="Extra Temperature 3", + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class="measurement", + entity_registry_enabled_default=False, + suggested_display_precision=1 + ), + SensorEntityDescription( + key="ExtraTemps04", + name="Extra Temperature 4", + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class="measurement", + entity_registry_enabled_default=False, + suggested_display_precision=1 + ), + SensorEntityDescription( + key="ExtraTemps05", + name="Extra Temperature 5", + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class="measurement", + entity_registry_enabled_default=False, + suggested_display_precision=1 + ), + SensorEntityDescription( + key="ExtraTemps06", + name="Extra Temperature 6", + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class="measurement", + entity_registry_enabled_default=False, + suggested_display_precision=1 + ), + SensorEntityDescription( + key="ExtraTemps07", + name="Extra Temperature 7", + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class="measurement", + entity_registry_enabled_default=False, + suggested_display_precision=1 + ) +] + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Davis Vantage sensors based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + entities: list[DavisVantageSensor] = [] + + # Add all meter sensors described above. + for description in DESCRIPTIONS: + entities.append( + DavisVantageSensor( + coordinator=coordinator, + entry_id=entry.entry_id, + description=description, + ) + ) + + async_add_entities(entities) + + +class DavisVantageSensor(CoordinatorEntity[DavisVantageDataUpdateCoordinator], SensorEntity): + """Defines a Davis Vantage sensor.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: DavisVantageDataUpdateCoordinator, + entry_id: str, + description: SensorEntityDescription, + ) -> None: + """Initialize Davis Vantage sensor.""" + super().__init__(coordinator=coordinator) + + self.entity_id = ( + f"{SENSOR_DOMAIN}.{DEFAULT_NAME}_{description.name}".lower() + ) + self.entity_description = description + self._attr_name = description.name # type: ignore + self._attr_unique_id = f"{entry_id}-{DEFAULT_NAME} {self.name}" + self._attr_device_info = coordinator.device_info + + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + key = self.entity_description.key + data = self.coordinator.data + if key not in data: + return None + if self.entity_description.native_unit_of_measurement != None: + default_value = 0 + else: + default_value = '-' + return data.get(key, default_value) # type: ignore diff --git a/custom_components/davis_vantage/strings.json b/custom_components/davis_vantage/strings.json new file mode 100755 index 0000000..704dc29 --- /dev/null +++ b/custom_components/davis_vantage/strings.json @@ -0,0 +1,36 @@ +{ + "config": { + "step": { + "user": { + "data": { + "link": "[%key:common::config_flow::data::link%]" + } + }, + "setup_serial": { + "data": { + "link": "[%key:common::config_flow::data::link%]" + } + }, + "setup_network": { + "data": { + "link": "[%key:common::config_flow::data::link%]" + } + }, + "setup_other_info": { + "data": { + "interval": "[%key:common::config_flow::data::interval%]", + "rain_collector": "[%key:common::config_flow::data::rain_collector%]", + "windrose8": "[%key:common::config_flow::data::windrose8%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/custom_components/davis_vantage/translations/en.json b/custom_components/davis_vantage/translations/en.json new file mode 100755 index 0000000..cde8750 --- /dev/null +++ b/custom_components/davis_vantage/translations/en.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Looks like the weather station isn't reacting, try again.", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "protocol": "Protocol", + "link": "Link", + "interval": "Interval", + "rain_collector": "Rain collector", + "windrose8": "Wind rose with 8 cardinal directions" + } + }, + "setup_serial": { + "data": { + "link": "Device" + } + }, + "setup_network": { + "data": { + "link": "Host" + } + }, + "setup_other_info": { + "data": { + "interval": "Interval", + "rain_collector": "Rain collector", + "windrose8": "Wind rose with 8 cardinal directions" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/davis_vantage/utils.py b/custom_components/davis_vantage/utils.py new file mode 100755 index 0000000..9a03e61 --- /dev/null +++ b/custom_components/davis_vantage/utils.py @@ -0,0 +1,383 @@ +import math +from datetime import datetime +# from time import strftime +from zoneinfo import ZoneInfo +from typing import Any + +ForecastStrings = ["Mostly clear and cooler.", +"Mostly clear with little temperature change.", +"Mostly clear for 12 hrs. with little temperature change.", +"Mostly clear for 12 to 24 hrs. and cooler.", +"Mostly clear with little temperature change.", +"Partly cloudy and cooler.", +"Partly cloudy with little temperature change.", +"Partly cloudy with little temperature change.", +"Mostly clear and warmer.", +"Partly cloudy with little temperature change.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Increasing clouds and warmer. Precipitation possible within 24 to 48 hrs.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Increasing clouds with little temperature change. Precipitation possible within 24 hrs.", +"Mostly clear with little temperature change.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Increasing clouds with little temperature change. Precipitation possible within 12 hrs.", +"Mostly clear with little temperature change.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Increasing clouds and warmer. Precipitation possible within 24 hrs.", +"Mostly clear and warmer. Increasing winds.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Increasing clouds and warmer. Precipitation possible within 12 hrs. Increasing winds.", +"Mostly clear and warmer. Increasing winds.", +"Increasing clouds and warmer.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Increasing clouds and warmer. Precipitation possible within 12 hrs. Increasing winds.", +"Mostly clear and warmer. Increasing winds.", +"Increasing clouds and warmer.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Increasing clouds and warmer. Precipitation possible within 12 hrs. Increasing winds.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Mostly clear and warmer. Precipitation possible within 48 hrs.", +"Mostly clear and warmer.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Increasing clouds with little temperature change. Precipitation possible within 24 to 48 hrs.", +"Increasing clouds with little temperature change.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Increasing clouds and warmer. Precipitation possible within 12 to 24 hrs.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Increasing clouds and warmer. Precipitation possible within 12 to 24 hrs. Windy.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Increasing clouds and warmer. Precipitation possible within 12 to 24 hrs. Windy.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Increasing clouds and warmer. Precipitation possible within 6 to 12 hrs.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Increasing clouds and warmer. Precipitation possible within 6 to 12 hrs. Windy.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Increasing clouds and warmer. Precipitation possible within 12 to 24 hrs. Windy.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Increasing clouds and warmer. Precipitation possible within 12 hrs.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Increasing clouds and warmer. Precipitation likely.", +"clearing and cooler. Precipitation ending within 6 hrs.", +"Partly cloudy with little temperature change.", +"clearing and cooler. Precipitation ending within 6 hrs.", +"Mostly clear with little temperature change.", +"Clearing and cooler. Precipitation ending within 6 hrs.", +"Partly cloudy and cooler.", +"Partly cloudy with little temperature change.", +"Mostly clear and cooler.", +"clearing and cooler. Precipitation ending within 6 hrs.", +"Mostly clear with little temperature change.", +"Clearing and cooler. Precipitation ending within 6 hrs.", +"Mostly clear and cooler.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Increasing clouds with little temperature change. Precipitation possible within 24 hrs.", +"Mostly cloudy and cooler. Precipitation continuing.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Mostly cloudy and cooler. Precipitation likely.", +"Mostly cloudy with little temperature change. Precipitation continuing.", +"Mostly cloudy with little temperature change. Precipitation likely.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Increasing clouds and cooler. Precipitation possible and windy within 6 hrs.", +"Increasing clouds with little temperature change. Precipitation possible and windy within 6 hrs.", +"Mostly cloudy and cooler. Precipitation continuing. Increasing winds.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Mostly cloudy and cooler. Precipitation likely. Increasing winds.", +"Mostly cloudy with little temperature change. Precipitation continuing. Increasing winds.", +"Mostly cloudy with little temperature change. Precipitation likely. Increasing winds.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Increasing clouds and cooler. Precipitation possible within 12 to 24 hrs. Possible wind shift to the W, NW, or N.", +"Increasing clouds with little temperature change. Precipitation possible within 12 to 24 hrs. Possible wind shift to the W, NW, or N.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Increasing clouds and cooler. Precipitation possible within 6 hrs. Possible wind shift to the W, NW, or N.", +"Increasing clouds with little temperature change. Precipitation possible within 6 hrs. Possible wind shift to the W, NW, or N.", +"Mostly cloudy and cooler. Precipitation ending within 12 hrs. Possible wind shift to the W, NW, or N.", +"Mostly cloudy and cooler. Possible wind shift to the W, NW, or N.", +"Mostly cloudy with little temperature change. Precipitation ending within 12 hrs. Possible wind shift to the W, NW, or N.", +"Mostly cloudy with little temperature change. Possible wind shift to the W, NW, or N.", +"Mostly cloudy and cooler. Precipitation ending within 12 hrs. Possible wind shift to the W, NW, or N.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Mostly cloudy and cooler. Precipitation possible within 24 hrs. Possible wind shift to the W, NW, or N.", +"Mostly cloudy with little temperature change. Precipitation ending within 12 hrs. Possible wind shift to the W, NW, or N.", +"Mostly cloudy with little temperature change. Precipitation possible within 24 hrs. Possible wind shift to the W, NW, or N.", +"clearing, cooler and windy. Precipitation ending within 6 hrs.", +"clearing, cooler and windy.", +"Mostly cloudy and cooler. Precipitation ending within 6 hrs. Windy with possible wind shift to the W, NW, or N.", +"Mostly cloudy and cooler. Windy with possible wind shift to the W, NW, or N.", +"clearing, cooler and windy.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Mostly cloudy with little temperature change. Precipitation possible within 12 hrs. Windy.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Increasing clouds and cooler. Precipitation possible within 12 hrs, possibly heavy at times. Windy.", +"Mostly cloudy and cooler. Precipitation ending within 6 hrs. Windy.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Mostly cloudy and cooler. Precipitation possible within 12 hrs. Windy.", +"Mostly cloudy and cooler. Precipitation ending in 12 to 24 hrs.", +"Mostly cloudy and cooler.", +"Mostly cloudy and cooler. Precipitation continuing, possible heavy at times. Windy.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Mostly cloudy and cooler. Precipitation possible within 6 to 12 hrs. Windy.", +"Mostly cloudy with little temperature change. Precipitation continuing, possibly heavy at times. Windy.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Mostly cloudy with little temperature change. Precipitation possible within 6 to 12 hrs. Windy.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Increasing clouds with little temperature change. Precipitation possible within 12 hrs, possibly heavy at times. Windy.", +"Mostly cloudy and cooler. Windy.", +"Mostly cloudy and cooler. Precipitation continuing, possibly heavy at times. Windy.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Mostly cloudy and cooler. Precipitation likely, possibly heavy at times. Windy.", +"Mostly cloudy with little temperature change. Precipitation continuing, possibly heavy at times. Windy.", +"Mostly cloudy with little temperature change. Precipitation likely, possibly heavy at times. Windy.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Increasing clouds and cooler. Precipitation possible within 6 hrs. Windy.", +"Increasing clouds with little temperature change. Precipitation possible within 6 hrs. windy", +"Increasing clouds and cooler. Precipitation continuing. Windy with possible wind shift to the W, NW, or N.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Mostly cloudy and cooler. Precipitation likely. Windy with possible wind shift to the W, NW, or N.", +"Mostly cloudy with little temperature change. Precipitation continuing. Windy with possible wind shift to the W, NW, or N.", +"Mostly cloudy with little temperature change. Precipitation likely. Windy with possible wind shift to the W, NW, or N.", +"Increasing clouds and cooler. Precipitation possible within 6 hrs. Windy with possible wind shift to the W, NW, or N.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Increasing clouds and cooler. Precipitation possible within 6 hrs. Possible wind shift to the W, NW, or N.", +"Increasing clouds with little temperature change. Precipitation possible within 6 hrs. Windy with possible wind shift to the W, NW, or N.", +"Increasing clouds with little temperature change. Precipitation possible within 6 hrs. Possible wind shift to the W, NW, or N.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Increasing clouds and cooler. Precipitation possible within 6 hrs. Windy with possible wind shift to the W, NW, or N.", +"Increasing clouds with little temperature change. Precipitation possible within 6 hrs. Windy with possible wind shift to the W, NW, or N.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Increasing clouds and cooler. Precipitation possible within 12 to 24 hrs. Windy with possible wind shift to the W, NW, or N.", +"Increasing clouds with little temperature change. Precipitation possible within 12 to 24 hrs. Windy with possible wind shift to the W, NW, or N.", +"Mostly cloudy and cooler. Precipitation possibly heavy at times and ending within 12 hrs. Windy with possible wind shift to the W, NW, or N.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Mostly cloudy and cooler. Precipitation possible within 6 to 12 hrs, possibly heavy at times. Windy with possible wind shift to the W, NW, or N.", +"Mostly cloudy with little temperature change. Precipitation ending within 12 hrs. Windy with possible wind shift to the W, NW, or N.", +"Mostly cloudy with little temperature change. Precipitation possible within 6 to 12 hrs, possibly heavy at times. Windy with possible wind shift to the W, NW, or N.", +"Mostly cloudy and cooler. Precipitation continuing.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Mostly cloudy and cooler. Precipitation likely, windy with possible wind shift to the W, NW, or N.", +"Mostly cloudy with little temperature change. Precipitation continuing.", +"Mostly cloudy with little temperature change. Precipitation likely.", +"Partly cloudy with little temperature change.", +"Mostly clear with little temperature change.", +"Mostly cloudy and cooler. Precipitation possible within 12 hours, possibly heavy at times. Windy.", +"FORECAST REQUIRES 3 HRS. OF RECENT DATA", +"Mostly clear and cooler." ] + +def convert_to_celcius(value: float) -> float: + return round((value - 32.0) * (5.0/9.0), 1) + +def convert_celcius_to_fahrenheit(value_c: float) -> float: + return round(value_c * 1.8 + 32, 1) + +def convert_to_kmh(value: float) -> float: + return round(value * 1.609344, 1) + +def convert_to_ms(value: float) -> float: + return convert_kmh_to_ms(convert_to_kmh(value)) + +def convert_to_mbar(value: float) -> float: + return round(value * 33.8637526, 1) + +def convert_to_mm(value: float) -> float: + return round(value * 20.0, 1) # Use metric tipping bucket modification + +def convert_kmh_to_ms(windspeed: float) -> float: + return round(windspeed / 3.6, 1) + +def convert_ms_to_bft(windspeed: float) -> int: + if windspeed < 0.2: + return 0 + elif windspeed < 1.6: + return 1 + elif windspeed < 3.4: + return 2 + elif windspeed < 5.5: + return 3 + elif windspeed < 8.0: + return 4 + elif windspeed < 10.8: + return 5 + elif windspeed < 13.9: + return 6 + elif windspeed < 17.2: + return 7 + elif windspeed < 20.8: + return 8 + elif windspeed < 24.5: + return 9 + elif windspeed < 28.5: + return 10 + elif windspeed < 32.7: + return 11 + else: + return 12 + +def convert_kmh_to_bft(windspeed_kmh: float) -> int: + return convert_ms_to_bft(convert_kmh_to_ms(windspeed_kmh)) + +def contains_correct_data(json_data: dict[str, Any]) -> None: + return json_data['OutsideTemp']['value'] < 60 \ + and json_data['RainRate']['value'] < 1000 \ + and json_data['WindSpeed']['value'] < 40 \ + and json_data['OutsideHum']['value'] < 100 \ + and json_data['WindAvgSpeed']['value'] < 250 + +def calc_heat_index(temperature_f: float, humidity: float) -> float: + if temperature_f < 80.0 or humidity < 40.0: + return temperature_f + else: + heat_index_f = \ + -42.379 \ + + (2.04901523 * temperature_f) \ + + (10.14333127 * humidity) \ + - (0.22475541 * temperature_f * humidity) \ + - (0.00683783 * pow(temperature_f, 2)) \ + - (0.05481717 * pow(humidity, 2)) \ + + (0.00122874 * pow(temperature_f, 2) * humidity) \ + + (0.00085282 * temperature_f * pow(humidity, 2)) \ + - (0.00000199 * pow(temperature_f, 2) * pow(humidity, 2)) + + if (heat_index_f < temperature_f): + heat_index_f = temperature_f + + return heat_index_f + +def calc_wind_chill(temperature_f: float, windspeed: float) -> float: + if (windspeed == 0): + wind_chill_f = temperature_f + else: + wind_chill_f = \ + 35.74 \ + + (0.6215 * temperature_f) \ + - (35.75 * pow(windspeed,0.16)) \ + + (0.4275 * temperature_f * pow(windspeed, 0.16)) + if (wind_chill_f > temperature_f): + wind_chill_f = temperature_f + + return wind_chill_f + +def calc_feels_like(temperature_f: float, humidity: float, windspeed_mph: float) -> float: + if windspeed_mph == 0: + windspeed_mph = 1 + feels_like_f = temperature_f + if temperature_f <= 50 and humidity >= 3: + feels_like_f = \ + 35.74 \ + + (0.6215 * temperature_f) \ + - (35.75 * pow(windspeed_mph, 0.16)) \ + + (0.4275 * temperature_f * pow(windspeed_mph, 0.16)) + + if feels_like_f == temperature_f and temperature_f >= 80: + feels_like_f = \ + 0.5 * (temperature_f + 61 + ((temperature_f - 68) * 1.2) \ + + (humidity * 0.094) ) + + if feels_like_f >= 80: + feels_like_f = \ + -42.379 \ + + (2.04901523 * temperature_f) \ + + (10.14333127 * humidity) \ + - (0.22475541 * temperature_f * humidity) \ + - (0.00683783 * pow(temperature_f, 2)) \ + - (0.05481717 * pow(humidity, 2)) \ + + (0.00122874 * pow(temperature_f, 2) * humidity) \ + + (0.00085282 * temperature_f * pow(humidity, 2)) \ + - (0.00000199 * pow(temperature_f, 2) * pow(humidity, 2)) + + if humidity < 13 and temperature_f >= 80 and temperature_f <= 112: + feels_like_f = feels_like_f - ((13 - humidity) / 4) * math.sqrt((17 - math.fabs(temperature_f - 95.0)) / 17) + + if humidity > 85 and temperature_f >= 80 and temperature_f <= 87: + feels_like_f = feels_like_f + ((humidity - 85) / 10) * ((87 - temperature_f) / 5) + return feels_like_f + +def convert_to_iso_datetime(value: datetime, tzinfo: ZoneInfo) -> datetime: + return value.replace(tzinfo=tzinfo) + +def get_wind_rose(bearing: int, windrose8: bool) -> str: + directions = [ 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', + 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW' ] + if windrose8: + index = (round(bearing / 45) % 8) * 2 + else: + index = round(bearing / 22.5) % 16 + return directions[index] + +def has_correct_value(value: float) -> bool: + return value != 255 and value != 32767 + +def round_to_one_decimal(value: float) -> float: + return round(value, 1) + +def get_baro_trend(trend: int) -> str: + if trend in [-60,196]: + return "Falling Rapidly" + elif trend in [-20,236]: + return "Falling Slowly" + elif trend == 0: + return "Steady" + elif trend == 20: + return "Rising Slowly" + elif trend == 60: + return "Rising Rapidly" + else: + return f"n/a ({trend})" + +def get_forecast_string(wrule: int) -> str: + if wrule > 194: + wrule = 194 + return ForecastStrings[wrule] + +def get_uv(value: int) -> float|bool: + if value == 255: + return False + else: + return round(value / 10, 1) + +def get_solar_rad(value: int) -> float|bool: + if value == 32767: + return False + else: + return value + +def calc_dew_point(temperature_f: float, humidity: float) -> float: + temperature_c = convert_to_celcius(temperature_f) + A = math.log(humidity / 100) + (17.62 * temperature_c / (243.12 + temperature_c)) + return convert_celcius_to_fahrenheit(243.12 * A / (17.62 - A)) diff --git a/hacs.json b/hacs.json new file mode 100755 index 0000000..1bc81f0 --- /dev/null +++ b/hacs.json @@ -0,0 +1,5 @@ +{ + "name": "Davis Vantage", + "homeassistant": "2022.12.0", + "render_readme": true + }