From 1979098fd740a7f87b75a05987b1cbad6df5b1a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20S=20-=20Piper?= Date: Sat, 16 Sep 2023 14:01:07 +0000 Subject: [PATCH 1/2] Add support for API V2 --- custom_components/weatherlink/__init__.py | 113 +++++++++- custom_components/weatherlink/config_flow.py | 117 ++++++++++- .../weatherlink/pyweatherlink.py | 88 +++++++- custom_components/weatherlink/sensor.py | 194 ++++++++++++------ .../weatherlink/translations/en.json | 60 ++++-- .../weatherlink/translations/sv.json | 64 ++++-- scripts/develop | 2 +- 7 files changed, 518 insertions(+), 120 deletions(-) diff --git a/custom_components/weatherlink/__init__.py b/custom_components/weatherlink/__init__.py index d8684bb..29a68ff 100644 --- a/custom_components/weatherlink/__init__.py +++ b/custom_components/weatherlink/__init__.py @@ -13,8 +13,9 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from .config_flow import API_V1, API_V2 from .const import DOMAIN -from .pyweatherlink import WLHub +from .pyweatherlink import WLHub, WLHubV2 PLATFORMS = [Platform.SENSOR] @@ -26,12 +27,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = {} - hass.data[DOMAIN][entry.entry_id]["api"] = WLHub( - websession=async_get_clientsession(hass), - username=entry.data["username"], - password=entry.data["password"], - apitoken=entry.data["apitoken"], - ) + if entry.data["api_version"] == API_V1: + hass.data[DOMAIN][entry.entry_id]["api"] = WLHub( + websession=async_get_clientsession(hass), + username=entry.data["username"], + password=entry.data["password"], + apitoken=entry.data["apitoken"], + ) + + if entry.data["api_version"] == API_V2: + hass.data[DOMAIN][entry.entry_id]["api"] = WLHubV2( + websession=async_get_clientsession(hass), + station_id=entry.data["station_id"], + api_key_v2=entry.data["api_key_v2"], + api_secret=entry.data["api_secret"], + ) + hass.data[DOMAIN][entry.entry_id]["station_data"] = await hass.data[DOMAIN][ + entry.entry_id + ]["api"].get_station() coordinator = await get_coordinator(hass, entry) if not coordinator.last_update_success: @@ -59,12 +72,79 @@ async def get_coordinator( if "coordinator" in hass.data[DOMAIN][entry.entry_id]: return hass.data[DOMAIN][entry.entry_id]["coordinator"] + def _preprocess(indata: str): + outdata = {} + _LOGGER.debug("Received data: %s", indata) + if entry.data["api_version"] == API_V1: + outdata["DID"] = indata["davis_current_observation"].get("DID") + outdata["station_name"] = indata["davis_current_observation"].get( + "station_name" + ) + outdata["temp_out"] = indata.get("temp_f") + outdata["temp_in"] = indata["davis_current_observation"].get("temp_in_f") + outdata["hum_in"] = indata["davis_current_observation"].get( + "relative_humidity_in" + ) + outdata["hum_out"] = indata.get("relative_humidity") + outdata["bar_sea_level"] = indata.get("pressure_in") + outdata["wind_mph"] = indata.get("wind_mph") + outdata["wind_dir"] = indata.get("wind_degrees") + outdata["dewpoint"] = indata.get("dewpoint_f") + outdata["rain_day"] = indata["davis_current_observation"].get("rain_day_in") + outdata["rain_rate"] = indata["davis_current_observation"].get( + "rain_rate_in_per_hr" + ) + outdata["rain_month"] = indata["davis_current_observation"].get( + "rain_month_in" + ) + outdata["rain_year"] = indata["davis_current_observation"].get( + "rain_year_in" + ) + if entry.data["api_version"] == API_V2: + outdata["station_id_uuid"] = indata["station_id_uuid"] + for sensor in indata["sensors"]: + if sensor["sensor_type"] == 37 and sensor["data_structure_type"] == 10: + outdata["temp_out"] = sensor["data"][0]["temp"] + outdata["hum_out"] = sensor["data"][0]["hum"] + outdata["wind_mph"] = sensor["data"][0]["wind_speed_last"] + outdata["wind_dir"] = sensor["data"][0]["wind_dir_last"] + outdata["dewpoint"] = sensor["data"][0]["dew_point"] + outdata["rain_day"] = float(sensor["data"][0]["rainfall_daily_in"]) + outdata["rain_rate"] = sensor["data"][0]["rain_rate_last_in"] + outdata["rain_month"] = sensor["data"][0]["rainfall_monthly_in"] + outdata["rain_year"] = sensor["data"][0]["rainfall_year_in"] + if sensor["sensor_type"] == 37 and sensor["data_structure_type"] == 23: + outdata["temp_out"] = sensor["data"][0]["temp"] + outdata["hum_out"] = sensor["data"][0]["hum"] + outdata["wind_mph"] = sensor["data"][0]["wind_speed_last"] + outdata["wind_dir"] = sensor["data"][0]["wind_dir_last"] + outdata["dewpoint"] = sensor["data"][0]["dew_point"] + outdata["rain_day"] = float(sensor["data"][0]["rainfall_day_in"]) + outdata["rain_rate"] = sensor["data"][0]["rain_rate_last_in"] + outdata["rain_month"] = sensor["data"][0]["rainfall_month_in"] + outdata["rain_year"] = sensor["data"][0]["rainfall_year_in"] + if sensor["sensor_type"] == 365 and sensor["data_structure_type"] == 21: + outdata["temp_in"] = sensor["data"][0]["temp_in"] + outdata["hum_in"] = sensor["data"][0]["hum_in"] + if sensor["sensor_type"] == 243 and sensor["data_structure_type"] == 12: + outdata["temp_in"] = sensor["data"][0]["temp_in"] + outdata["hum_in"] = sensor["data"][0]["hum_in"] + if sensor["sensor_type"] == 242 and sensor["data_structure_type"] == 12: + outdata["bar_sea_level"] = sensor["data"][0]["bar_sea_level"] + outdata["bar_trend"] = sensor["data"][0]["bar_trend"] + if sensor["sensor_type"] == 242 and sensor["data_structure_type"] == 19: + outdata["bar_sea_level"] = sensor["data"][0]["bar_sea_level"] + outdata["bar_trend"] = sensor["data"][0]["bar_trend"] + + return outdata + async def async_fetch(): api = hass.data[DOMAIN][entry.entry_id]["api"] try: async with async_timeout.timeout(10): res = await api.request("GET") - return await res.json() + json_data = await res.json() + return _preprocess(json_data) except ClientResponseError as exc: _LOGGER.warning("API fetch failed. Status: %s, - %s", exc.code, exc.message) raise UpdateFailed(exc) from exc @@ -78,3 +158,20 @@ async def async_fetch(): ) await hass.data[DOMAIN][entry.entry_id]["coordinator"].async_refresh() return hass.data[DOMAIN][entry.entry_id]["coordinator"] + + +async def async_migrate_entry(hass, config_entry: ConfigEntry): + """Migrate old entry.""" + _LOGGER.info("Migrating from version %s", config_entry.version) + + if config_entry.version == 1: + new_data = {**config_entry.data} + + new_data["api_version"] = API_V1 + + config_entry.version = 2 + hass.config_entries.async_update_entry(config_entry, data=new_data) + + _LOGGER.info("Migration to version %s successful", config_entry.version) + + return True diff --git a/custom_components/weatherlink/config_flow.py b/custom_components/weatherlink/config_flow.py index 159fd95..f02a118 100644 --- a/custom_components/weatherlink/config_flow.py +++ b/custom_components/weatherlink/config_flow.py @@ -14,12 +14,25 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN -from .pyweatherlink import WLHub +from .pyweatherlink import WLHub, WLHubV2 _LOGGER = logging.getLogger(__name__) -# TODO adjust the data schema to the data that you need -STEP_USER_DATA_SCHEMA = vol.Schema( +API_V1 = "api_v1" +API_V2 = "api_v2" +API_VERSIONS = [API_V1, API_V2] + +STEP_USER_APIVER_SCHEMA = vol.Schema( + { + vol.Required("api_version", default=API_V2): selector.SelectSelector( + selector.SelectSelectorConfig( + options=API_VERSIONS, translation_key="set_api_ver" + ) + ), + } +) + +STEP_USER_DATA_SCHEMA_V1 = vol.Schema( { vol.Required("username"): selector.TextSelector(), vol.Required("password"): str, @@ -27,11 +40,19 @@ } ) +STEP_USER_DATA_SCHEMA_V2 = vol.Schema( + { + vol.Required("station_id"): selector.TextSelector(), + vol.Required("api_key_v2"): selector.TextSelector(), + vol.Required("api_secret"): selector.TextSelector(), + } +) + 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. + Data has the keys from STEP_USER_DATA_SCHEMA_V1 with values provided by the user. """ # TODO validate the data can be used to set up a connection. @@ -65,22 +86,72 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, return {"title": station_name, "did": did} +async def validate_input_v2( + 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_V2 with values provided by the user. + """ + # TODO validate the data can be used to set up a connection. + + websession = async_get_clientsession(hass) + hub = WLHubV2( + station_id=data["station_id"], + api_key_v2=data["api_key_v2"], + api_secret=data["api_secret"], + websession=websession, + ) + + if not await hub.authenticate( + data["station_id"], data["api_key_v2"], data["api_secret"] + ): + raise InvalidAuth + + # If you cannot connect: + # throw CannotConnect + # If the authentication is wrong: + # InvalidAuth + + # Return info that you want to store in the config entry. + data = await hub.get_station() + _LOGGER.debug("Station data: %s", data) + station_name = data["stations"][0]["station_name"] + # did = data["davis_current_observation"]["DID"] + + # return {"title": station_name, "did": did} + return {"title": station_name} + + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Weatherlink.""" - VERSION = 1 + VERSION = 2 async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the initial step.""" + data_schema = STEP_USER_APIVER_SCHEMA if user_input is None: - return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA - ) + return self.async_show_form(step_id="user", data_schema=data_schema) + + if user_input["api_version"] == API_V1: + return await self.async_step_user_1() + if user_input["api_version"] == API_V2: + return await self.async_step_user_2() + + async def async_step_user_1( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + data_schema = STEP_USER_DATA_SCHEMA_V1 + if user_input is None: + return self.async_show_form(step_id="user_1", data_schema=data_schema) errors = {} + user_input["api_version"] = API_V1 try: info = await validate_input(self.hass, user_input) except CannotConnect: @@ -95,7 +166,35 @@ async def async_step_user( return self.async_create_entry(title=info["title"], data=user_input) return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + step_id="user_1", data_schema=data_schema, errors=errors + ) + + async def async_step_user_2( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + data_schema = STEP_USER_DATA_SCHEMA_V2 + if user_input is None: + return self.async_show_form(step_id="user_2", data_schema=data_schema) + + errors = {} + + user_input["api_version"] = API_V2 + try: + info = await validate_input_v2(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(user_input["station_id"]) + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user_2", data_schema=data_schema, errors=errors ) diff --git a/custom_components/weatherlink/pyweatherlink.py b/custom_components/weatherlink/pyweatherlink.py index 13f2e5e..5192174 100644 --- a/custom_components/weatherlink/pyweatherlink.py +++ b/custom_components/weatherlink/pyweatherlink.py @@ -2,12 +2,16 @@ Move to pypi.org when stable """ +from dataclasses import dataclass import logging import urllib.parse from aiohttp import ClientResponse, ClientResponseError, ClientSession -API_URL = "https://api.weatherlink.com/v1/NoaaExt.json" +from .const import VERSION + +API_V1_URL = "https://api.weatherlink.com/v1/NoaaExt.json" +API_V2_URL = "https://api.weatherlink.com/v2/" _LOGGER = logging.getLogger(__name__) @@ -48,7 +52,68 @@ async def request(self, method, **kwargs) -> ClientResponse: res = await self.websession.request( method, - f"{API_URL}?{params_enc}", + f"{API_V1_URL}?{params_enc}", + **kwargs, + headers=headers, + ) + res.raise_for_status() + return res + + async def get_data(self): + """Get data from api.""" + try: + res = await self.request("GET") + return await res.json() + except ClientResponseError as exc: + _LOGGER.error( + "API get_data failed. Status: %s, - %s", exc.code, exc.message + ) + + +class WLHubV2: + """Class to get data from Wetherlink API v2.""" + + def __init__( + self, + station_id: str, + api_key_v2: str, + api_secret: str, + websession: ClientSession, + ) -> None: + """Initialize.""" + self.station_id = station_id + self.api_key_v2 = api_key_v2 + self.api_secret = api_secret + self.websession = websession + + async def authenticate( + self, station_id: str, api_key_v2: str, api_secret: str + ) -> bool: + """Test if we can authenticate with the host.""" + return True + + async def request(self, method, endpoint="current/", **kwargs) -> ClientResponse: + """Make a request.""" + headers = kwargs.get("headers") + + if headers is None: + headers = {} + else: + headers = dict(headers) + kwargs.pop("headers") + + headers["x-api-secret"] = self.api_secret + headers["User-Agent"] = f"Weatherlink for Home Assistant/{VERSION}" + params = { + # "station_id": self.station_id, + "api-key": self.api_key_v2, + # "api_secret": self.api_secret, + } + params_enc = urllib.parse.urlencode(params, quote_via=urllib.parse.quote) + + res = await self.websession.request( + method, + f"{API_V2_URL}{endpoint}{self.station_id}?{params_enc}", **kwargs, headers=headers, ) @@ -64,3 +129,22 @@ async def get_data(self): _LOGGER.error( "API get_data failed. Status: %s, - %s", exc.code, exc.message ) + + async def get_station(self): + """Get data from api.""" + try: + res = await self.request("GET", endpoint="stations/") + return await res.json() + except ClientResponseError as exc: + _LOGGER.error( + "API get_station failed. Status: %s, - %s", exc.code, exc.message + ) + + +@dataclass +class WLData: + """Common data model for all API:s and stations.""" + + temp_out: float | None = None + temp_in: float | None = None + humidity_out: float | None = None diff --git a/custom_components/weatherlink/sensor.py b/custom_components/weatherlink/sensor.py index 9c00306..eeae7e6 100644 --- a/custom_components/weatherlink/sensor.py +++ b/custom_components/weatherlink/sensor.py @@ -3,6 +3,7 @@ from collections.abc import Callable from dataclasses import dataclass +import logging from typing import Any, Final from homeassistant.components.sensor import ( @@ -26,7 +27,11 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import get_coordinator +from .config_flow import API_V1, API_V2 from .const import DOMAIN +from .pyweatherlink import WLData + +_LOGGER = logging.getLogger(__name__) SUBTAG_1 = "davis_current_observation" @@ -44,15 +49,25 @@ class WLSensorDescription(SensorEntityDescription): SENSOR_TYPES: Final[tuple[WLSensorDescription, ...]] = ( WLSensorDescription( key="OutsideTemp", - tag="temp_c", + tag="temp_out", device_class=SensorDeviceClass.TEMPERATURE, + suggested_display_precision=1, translation_key="outside_temperature", - native_unit_of_measurement=UnitOfTemperature.CELSIUS, + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + state_class=SensorStateClass.MEASUREMENT, + ), + WLSensorDescription( + key="InsideTemp", + tag="temp_in", + device_class=SensorDeviceClass.TEMPERATURE, + translation_key="inside_temperature", + suggested_display_precision=1, + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, state_class=SensorStateClass.MEASUREMENT, ), WLSensorDescription( key="OutsideHumidity", - tag="relative_humidity", + tag="hum_out", device_class=SensorDeviceClass.HUMIDITY, suggested_display_precision=0, translation_key="outside_humidity", @@ -61,8 +76,7 @@ class WLSensorDescription(SensorEntityDescription): ), WLSensorDescription( key="InsideHumidity", - tag="relative_humidity_in", - subtag=SUBTAG_1, + tag="hum_in", device_class=SensorDeviceClass.HUMIDITY, suggested_display_precision=0, translation_key="inside_humidity", @@ -71,21 +85,20 @@ class WLSensorDescription(SensorEntityDescription): ), WLSensorDescription( key="Pressure", - tag="pressure_mb", + tag="bar_sea_level", device_class=SensorDeviceClass.PRESSURE, translation_key="pressure", suggested_display_precision=0, - native_unit_of_measurement=UnitOfPressure.MBAR, + native_unit_of_measurement=UnitOfPressure.INHG, state_class=SensorStateClass.MEASUREMENT, - decimals=0, ), WLSensorDescription( key="Wind", tag="wind_mph", device_class=SensorDeviceClass.WIND_SPEED, translation_key="wind", - convert=lambda x: x * 1609 / 3600, - native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, + suggested_display_precision=1, + native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, ), WLSensorDescription( @@ -94,61 +107,50 @@ class WLSensorDescription(SensorEntityDescription): icon="mdi:compass-outline", translation_key="wind_direction", ), - WLSensorDescription( - key="InsideTemp", - tag="temp_in_f", - subtag=SUBTAG_1, - device_class=SensorDeviceClass.TEMPERATURE, - translation_key="inside_temperature", - native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, - state_class=SensorStateClass.MEASUREMENT, - ), WLSensorDescription( key="RainToday", - tag="rain_day_in", - subtag=SUBTAG_1, + tag="rain_day", translation_key="rain_today", device_class=SensorDeviceClass.PRECIPITATION, - native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS, - convert=lambda x: x * 25.4, + suggested_display_precision=1, + native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, state_class=SensorStateClass.TOTAL_INCREASING, ), WLSensorDescription( key="RainRate", - tag="rain_rate_in_per_hr", + tag="rain_rate", subtag=SUBTAG_1, translation_key="rain_rate", device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, - native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, - convert=lambda x: x * 25.4, + native_unit_of_measurement=UnitOfVolumetricFlux.INCHES_PER_HOUR, + suggested_display_precision=1, state_class=SensorStateClass.MEASUREMENT, ), WLSensorDescription( key="RainInMonth", - tag="rain_month_in", - subtag=SUBTAG_1, + tag="rain_month", translation_key="rain_this_month", + suggested_display_precision=0, device_class=SensorDeviceClass.PRECIPITATION, - native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS, - convert=lambda x: x * 25.4, + native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, state_class=SensorStateClass.TOTAL_INCREASING, ), WLSensorDescription( key="RainInYear", - tag="rain_year_in", - subtag=SUBTAG_1, + tag="rain_year", translation_key="rain_this_year", device_class=SensorDeviceClass.PRECIPITATION, - native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS, - convert=lambda x: x * 25.4, + suggested_display_precision=0, + native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, state_class=SensorStateClass.TOTAL_INCREASING, ), WLSensorDescription( key="Dewpoint", - tag="dewpoint_c", + tag="dewpoint", translation_key="dewpoint", device_class=SensorDeviceClass.TEMPERATURE, - native_unit_of_measurement=UnitOfTemperature.CELSIUS, + suggested_display_precision=1, + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, state_class=SensorStateClass.MEASUREMENT, ), ) @@ -163,7 +165,8 @@ async def async_setup_entry( coordinator = await get_coordinator(hass, config_entry) async_add_entities( - WLSensor(coordinator, description) for description in SENSOR_TYPES + WLSensor(coordinator, hass, config_entry, description) + for description in SENSOR_TYPES ) @@ -171,51 +174,110 @@ class WLSensor(CoordinatorEntity, SensorEntity): """Representation of a Sensor.""" entity_description: WLSensorDescription + sensor_data = WLData() - def __init__(self, coordinator, description: WLSensorDescription): + def __init__( + self, + coordinator, + hass: HomeAssistant, + entry: ConfigEntry, + description: WLSensorDescription, + ): """Initialize the sensor.""" super().__init__(coordinator) + self.hass = hass + self.entry: ConfigEntry = entry self.entity_description = description self._attr_has_entity_name = True self._attr_unique_id = ( - f"{self.coordinator.data[SUBTAG_1]['DID']}-{self.entity_description.key}" + f"{self.get_unique_id_base()}-{self.entity_description.key}" ) self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self.coordinator.data[SUBTAG_1]["DID"])}, - name=self.coordinator.data[SUBTAG_1]["station_name"], + identifiers={(DOMAIN, self.get_unique_id_base())}, + name=self.generate_name(), manufacturer="Davis", - model="Weatherlink", + model=self.generate_model(), configuration_url="https://www.weatherlink.com/", ) + def get_unique_id_base(self): + """Generate base for unique_id.""" + unique_base = None + if self.entry.data["api_version"] == API_V1: + unique_base = self.coordinator.data["DID"] + if self.entry.data["api_version"] == API_V2: + unique_base = self.coordinator.data["station_id_uuid"] + return unique_base + + def generate_name(self): + """Generate device name.""" + if self.entry.data["api_version"] == API_V1: + return self.coordinator.data["station_name"] + if self.entry.data["api_version"] == API_V2: + return self.hass.data[DOMAIN][self.entry.entry_id]["station_data"][ + "stations" + ][0]["station_name"] + + return "Unknown devicename" + + def generate_model(self): + """Generate model string.""" + if self.entry.data["api_version"] == API_V1: + return "Weatherlink - API V1" + if self.entry.data["api_version"] == API_V2: + model = self.hass.data[DOMAIN][self.entry.entry_id]["station_data"][ + "stations" + ][0].get("product_number") + return f"Weatherlink {model}" + return "Weatherlink" + @property def native_value(self): """Return the state of the sensor.""" - if self.entity_description.subtag is not None: - if ( - self.coordinator.data[self.entity_description.subtag].get( - self.entity_description.tag + # _LOGGER.debug("Key: %s", self.entity_description.key) + if self.entity_description.key in [ + "OutsideTemp", + "InsideTemp", + "OutsideHumidity", + "InsideHumidity", + "Pressure", + "Wind", + "Dewpoint", + "RainToday", + "RainRate", + "RainInYear", + "RainInMonth", + ]: + return self.coordinator.data.get(self.entity_description.tag) + + if self.entity_description.tag in ["wind_dir"]: + directions = [ + "n", + "nne", + "ne", + "ene", + "e", + "ese", + "se", + "sse", + "s", + "ssw", + "sw", + "wsw", + "w", + "wnw", + "nw", + "nnw", + ] + + index = int( + ( + (float(self.coordinator.data[self.entity_description.tag]) + 11.25) + % 360 ) - is None - ): - return None - value = float( - self.coordinator.data[self.entity_description.subtag][ - self.entity_description.tag - ] + // 22.5 ) - else: - if self.entity_description.tag in ["wind_dir"]: - return ( - self.coordinator.data.get(self.entity_description.tag) - .replace("-", "") - .lower() - ) - if self.coordinator.data.get(self.entity_description.tag) is None: - return None - value = float(self.coordinator.data[self.entity_description.tag]) + return directions[index] - if self.entity_description.convert is not None: - value = self.entity_description.convert(value) - return round(value, self.entity_description.decimals) + return None diff --git a/custom_components/weatherlink/translations/en.json b/custom_components/weatherlink/translations/en.json index 480d30d..889f97b 100644 --- a/custom_components/weatherlink/translations/en.json +++ b/custom_components/weatherlink/translations/en.json @@ -10,6 +10,15 @@ }, "step": { "user": { + "description": "Select the API version", + "data": { + "api_version": "API version" + }, + "data_description": { + "api_version": "V1 can be used for legacy devices, V2 can be used for all devices." + } + }, + "user_1": { "description": "Enter credentials to access your Weatherlink account", "data": { "apitoken": "API token v1", @@ -19,6 +28,25 @@ "data_description": { "username": "Use DID here if you have access to it - i.e. if it is your own station" } + }, + "user_2": { + "description": "Enter credentials to access your Weatherlink account", + "data": { + "station_id": "Station Id", + "api_key_v2": "API key v2", + "api_secret": "API secret" + }, + "data_description": { + "station_id": "Both integer or UUID station id types are accepted." + } + } + } + }, + "selector": { + "set_api_ver": { + "options": { + "api_v1": "API V1", + "api_v2": "API V2" } } }, @@ -45,22 +73,22 @@ "wind_direction": { "name": "Wind direction", "state": { - "north": "North", - "northnortheast": "North-northeast", - "northeast": "Northeast", - "eastnortheast": "East-northeast", - "east": "East", - "eastsoutheast": "East-southeast", - "southeast": "Southeast", - "southsoutheast": "South-southeast", - "south": "South", - "southsouthwest": "South-southwest", - "southwest": "Southwest", - "westsouthwest": "West-southwest", - "west": "West", - "westnorthwest": "West-northwest", - "northwest": "Northwest", - "northnorthwest": "North-Northwest" + "n": "N", + "nne": "NNE", + "ne": "NE", + "ene": "ENE", + "e": "E", + "ese": "ESE", + "se": "SE", + "sse": "SSE", + "s": "S", + "ssw": "SSW", + "sw": "SW", + "wsw": "WSW", + "w": "W", + "wnw": "WNW", + "nw": "NW", + "nnw": "NNW" } }, "rain_today": { diff --git a/custom_components/weatherlink/translations/sv.json b/custom_components/weatherlink/translations/sv.json index 6551b86..ec6bd70 100644 --- a/custom_components/weatherlink/translations/sv.json +++ b/custom_components/weatherlink/translations/sv.json @@ -10,15 +10,43 @@ }, "step": { "user": { - "description": "Ange inloggningsuppgifter till ditt Weatherlink-konto", + "description": "Välj API-version", + "data": { + "api_version": "API version" + }, + "data_description": { + "api_version": "V1 can användas för äldre enheter, V2 kan användas för alla enheter." + } + }, + "user_1": { + "description": "Ange åtkomstuppgifter för ditt Weatherlink-konto", "data": { "apitoken": "API token v1", "password": "Lösenord", "username": "Användarnamn eller DID" }, "data_description": { - "username": "Använd DID om du har tillgång till det, d v s om det är din egen station" + "username": "Använd DID här om du har tillgång till det - dvs om det är din egen station" } + }, + "user_2": { + "description": "Ange åtkomstuppgifter för din Weatherlink-station", + "data": { + "station_id": "Station-id", + "api_key_v2": "API key v2", + "api_secret": "API secret" + }, + "data_description": { + "station_id": "Stations-id kan anges som heltal eller UUID." + } + } + } + }, + "selector": { + "set_api_ver": { + "options": { + "api_v1": "API V1", + "api_v2": "API V2" } } }, @@ -45,22 +73,22 @@ "wind_direction": { "name": "Vindriktning", "state": { - "north": "Nord", - "northnortheast": "Nordnordost", - "northeast": "Nordost", - "eastnortheast": "Ostnordost", - "east": "Ost", - "eastsoutheast": "Ostsydost", - "southeast": "Sydost", - "southsoutheast": "Sydsydost", - "south": "Syd", - "southsouthwest": "Sydsydväst", - "southwest": "Sydväst", - "westsouthwest": "Västsydväst", - "west": "Väst", - "westnorthwest": "Västnordväst", - "northwest": "Nordväst", - "northnorthwest": "Nordnordväst" + "n": "N", + "nne": "NNO", + "ne": "NO", + "ene": "ONO", + "e": "O", + "ese": "OSO", + "se": "SO", + "sse": "SSO", + "s": "S", + "ssw": "SSV", + "sw": "SV", + "wsw": "VSV", + "w": "V", + "wnw": "VNV", + "nw": "NV", + "nnw": "NNv" } }, "rain_today": { diff --git a/scripts/develop b/scripts/develop index ea3c228..56cdf83 100755 --- a/scripts/develop +++ b/scripts/develop @@ -14,7 +14,7 @@ echo -n " logger: default: info logs: - homeassistant.components.viva: debug + custom_components.weatherlink: debug " >> config/configuration.yaml fi if ! grep -R "debugpy:" config/configuration.yaml >> /dev/null;then From 9a7c60a52702ad64b4f8754fefd8654e021731b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20S=20-=20Piper?= Date: Sun, 17 Sep 2023 08:23:23 +0000 Subject: [PATCH 2/2] More work on API V2 --- custom_components/weatherlink/__init__.py | 50 +++++-- .../weatherlink/binary_sensor.py | 127 ++++++++++++++++++ custom_components/weatherlink/config_flow.py | 125 +++++++++++++---- custom_components/weatherlink/const.py | 6 +- custom_components/weatherlink/diagnostics.py | 11 +- .../weatherlink/pyweatherlink.py | 23 +++- custom_components/weatherlink/sensor.py | 36 +++-- .../weatherlink/translations/en.json | 3 + .../weatherlink/translations/sv.json | 3 + 9 files changed, 325 insertions(+), 59 deletions(-) create mode 100644 custom_components/weatherlink/binary_sensor.py diff --git a/custom_components/weatherlink/__init__.py b/custom_components/weatherlink/__init__.py index 29a68ff..45a15b9 100644 --- a/custom_components/weatherlink/__init__.py +++ b/custom_components/weatherlink/__init__.py @@ -14,10 +14,17 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .config_flow import API_V1, API_V2 -from .const import DOMAIN +from .const import ( + CONF_API_KEY_V2, + CONF_API_SECRET, + CONF_API_TOKEN, + CONF_API_VERSION, + CONF_STATION_ID, + DOMAIN, +) from .pyweatherlink import WLHub, WLHubV2 -PLATFORMS = [Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) @@ -27,20 +34,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = {} - if entry.data["api_version"] == API_V1: + if entry.data[CONF_API_VERSION] == API_V1: hass.data[DOMAIN][entry.entry_id]["api"] = WLHub( websession=async_get_clientsession(hass), username=entry.data["username"], password=entry.data["password"], - apitoken=entry.data["apitoken"], + apitoken=entry.data[CONF_API_TOKEN], ) - if entry.data["api_version"] == API_V2: + if entry.data[CONF_API_VERSION] == API_V2: hass.data[DOMAIN][entry.entry_id]["api"] = WLHubV2( websession=async_get_clientsession(hass), - station_id=entry.data["station_id"], - api_key_v2=entry.data["api_key_v2"], - api_secret=entry.data["api_secret"], + station_id=entry.data[CONF_STATION_ID], + api_key_v2=entry.data[CONF_API_KEY_V2], + api_secret=entry.data[CONF_API_SECRET], ) hass.data[DOMAIN][entry.entry_id]["station_data"] = await hass.data[DOMAIN][ entry.entry_id @@ -74,8 +81,8 @@ async def get_coordinator( def _preprocess(indata: str): outdata = {} - _LOGGER.debug("Received data: %s", indata) - if entry.data["api_version"] == API_V1: + # _LOGGER.debug("Received data: %s", indata) + if entry.data[CONF_API_VERSION] == API_V1: outdata["DID"] = indata["davis_current_observation"].get("DID") outdata["station_name"] = indata["davis_current_observation"].get( "station_name" @@ -100,7 +107,7 @@ def _preprocess(indata: str): outdata["rain_year"] = indata["davis_current_observation"].get( "rain_year_in" ) - if entry.data["api_version"] == API_V2: + if entry.data[CONF_API_VERSION] == API_V2: outdata["station_id_uuid"] = indata["station_id_uuid"] for sensor in indata["sensors"]: if sensor["sensor_type"] == 37 and sensor["data_structure_type"] == 10: @@ -113,6 +120,21 @@ def _preprocess(indata: str): outdata["rain_rate"] = sensor["data"][0]["rain_rate_last_in"] outdata["rain_month"] = sensor["data"][0]["rainfall_monthly_in"] outdata["rain_year"] = sensor["data"][0]["rainfall_year_in"] + outdata["trans_battery_flag"] = sensor["data"][0][ + "trans_battery_flag" + ] + if sensor["sensor_type"] == 37 and sensor["data_structure_type"] == 2: + outdata["temp_out"] = sensor["data"][0]["temp_out"] + outdata["temp_in"] = sensor["data"][0]["temp_in"] + outdata["bar_sea_level"] = sensor["data"][0]["bar"] + outdata["hum_out"] = sensor["data"][0]["hum_out"] + outdata["wind_mph"] = sensor["data"][0]["wind_speed"] + outdata["wind_dir"] = sensor["data"][0]["wind_dir"] + outdata["dewpoint"] = sensor["data"][0]["dew_point"] + outdata["rain_day"] = float(sensor["data"][0]["rain_day_in"]) + outdata["rain_rate"] = sensor["data"][0]["rain_rate_in"] + outdata["rain_month"] = sensor["data"][0]["rain_month_in"] + outdata["rain_year"] = sensor["data"][0]["rain_year_in"] if sensor["sensor_type"] == 37 and sensor["data_structure_type"] == 23: outdata["temp_out"] = sensor["data"][0]["temp"] outdata["hum_out"] = sensor["data"][0]["hum"] @@ -123,6 +145,9 @@ def _preprocess(indata: str): outdata["rain_rate"] = sensor["data"][0]["rain_rate_last_in"] outdata["rain_month"] = sensor["data"][0]["rainfall_month_in"] outdata["rain_year"] = sensor["data"][0]["rainfall_year_in"] + outdata["trans_battery_flag"] = sensor["data"][0][ + "trans_battery_flag" + ] if sensor["sensor_type"] == 365 and sensor["data_structure_type"] == 21: outdata["temp_in"] = sensor["data"][0]["temp_in"] outdata["hum_in"] = sensor["data"][0]["hum_in"] @@ -144,6 +169,7 @@ async def async_fetch(): async with async_timeout.timeout(10): res = await api.request("GET") json_data = await res.json() + hass.data[DOMAIN][entry.entry_id]["current"] = json_data return _preprocess(json_data) except ClientResponseError as exc: _LOGGER.warning("API fetch failed. Status: %s, - %s", exc.code, exc.message) @@ -167,7 +193,7 @@ async def async_migrate_entry(hass, config_entry: ConfigEntry): if config_entry.version == 1: new_data = {**config_entry.data} - new_data["api_version"] = API_V1 + new_data[CONF_API_VERSION] = API_V1 config_entry.version = 2 hass.config_entries.async_update_entry(config_entry, data=new_data) diff --git a/custom_components/weatherlink/binary_sensor.py b/custom_components/weatherlink/binary_sensor.py new file mode 100644 index 0000000..53d6b59 --- /dev/null +++ b/custom_components/weatherlink/binary_sensor.py @@ -0,0 +1,127 @@ +"""Platform for binary sensor integration.""" +from __future__ import annotations + +from dataclasses import dataclass +import logging +from typing import Final + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import get_coordinator +from .config_flow import API_V1, API_V2 +from .const import CONF_API_VERSION, DOMAIN +from .pyweatherlink import WLData + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class WLBinarySensorDescription(BinarySensorEntityDescription): + """Class describing Weatherlink binarysensor entities.""" + + tag: str | None = None + exclude: set = () + + +SENSOR_TYPES: Final[tuple[WLBinarySensorDescription, ...]] = ( + WLBinarySensorDescription( + key="TransmitterBattery", + tag="trans_battery_flag", + device_class=BinarySensorDeviceClass.BATTERY, + translation_key="trans_battery", + entity_category=EntityCategory.DIAGNOSTIC, + exclude=(API_V1,), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the binary sensor platform.""" + coordinator = await get_coordinator(hass, config_entry) + + async_add_entities( + WLSensor(coordinator, hass, config_entry, description) + for description in SENSOR_TYPES + if config_entry.data[CONF_API_VERSION] not in description.exclude + ) + + +class WLSensor(CoordinatorEntity, BinarySensorEntity): + """Representation of a Binary Sensor.""" + + entity_description: WLBinarySensorDescription + sensor_data = WLData() + + def __init__( + self, + coordinator, + hass: HomeAssistant, + entry: ConfigEntry, + description: WLBinarySensorDescription, + ): + """Initialize the sensor.""" + super().__init__(coordinator) + self.hass = hass + self.entry: ConfigEntry = entry + self.entity_description = description + self._attr_has_entity_name = True + self._attr_unique_id = ( + f"{self.get_unique_id_base()}-{self.entity_description.key}" + ) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.get_unique_id_base())}, + name=self.generate_name(), + manufacturer="Davis", + model=self.generate_model(), + configuration_url="https://www.weatherlink.com/", + ) + + def get_unique_id_base(self): + """Generate base for unique_id.""" + unique_base = None + if self.entry.data[CONF_API_VERSION] == API_V1: + unique_base = self.coordinator.data["DID"] + if self.entry.data[CONF_API_VERSION] == API_V2: + unique_base = self.coordinator.data["station_id_uuid"] + return unique_base + + def generate_name(self): + """Generate device name.""" + if self.entry.data[CONF_API_VERSION] == API_V1: + return self.coordinator.data["station_name"] + if self.entry.data[CONF_API_VERSION] == API_V2: + return self.hass.data[DOMAIN][self.entry.entry_id]["station_data"][ + "stations" + ][0]["station_name"] + + return "Unknown devicename" + + def generate_model(self): + """Generate model string.""" + if self.entry.data[CONF_API_VERSION] == API_V1: + return "Weatherlink - API V1" + if self.entry.data[CONF_API_VERSION] == API_V2: + model = self.hass.data[DOMAIN][self.entry.entry_id]["station_data"][ + "stations" + ][0].get("product_number") + return f"Weatherlink {model}" + return "Weatherlink" + + @property + def is_on(self): + """Return the state of the sensor.""" + # _LOGGER.debug("Key: %s", self.entity_description.key) + return self.coordinator.data.get(self.entity_description.tag) diff --git a/custom_components/weatherlink/config_flow.py b/custom_components/weatherlink/config_flow.py index f02a118..9025546 100644 --- a/custom_components/weatherlink/config_flow.py +++ b/custom_components/weatherlink/config_flow.py @@ -10,10 +10,22 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import selector from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.selector import ( + SelectOptionDict, + SelectSelector, + SelectSelectorConfig, + TextSelector, +) -from .const import DOMAIN +from .const import ( + CONF_API_KEY_V2, + CONF_API_SECRET, + CONF_API_TOKEN, + CONF_API_VERSION, + CONF_STATION_ID, + DOMAIN, +) from .pyweatherlink import WLHub, WLHubV2 _LOGGER = logging.getLogger(__name__) @@ -24,27 +36,30 @@ STEP_USER_APIVER_SCHEMA = vol.Schema( { - vol.Required("api_version", default=API_V2): selector.SelectSelector( - selector.SelectSelectorConfig( - options=API_VERSIONS, translation_key="set_api_ver" - ) + vol.Required(CONF_API_VERSION, default=API_V2): SelectSelector( + SelectSelectorConfig(options=API_VERSIONS, translation_key="set_api_ver") ), } ) STEP_USER_DATA_SCHEMA_V1 = vol.Schema( { - vol.Required("username"): selector.TextSelector(), + vol.Required("username"): TextSelector(), vol.Required("password"): str, - vol.Required("apitoken"): str, + vol.Required(CONF_API_TOKEN): str, } ) -STEP_USER_DATA_SCHEMA_V2 = vol.Schema( +STEP_USER_DATA_SCHEMA_V2_A = vol.Schema( { - vol.Required("station_id"): selector.TextSelector(), - vol.Required("api_key_v2"): selector.TextSelector(), - vol.Required("api_secret"): selector.TextSelector(), + vol.Required(CONF_API_KEY_V2): TextSelector(), + vol.Required(CONF_API_SECRET): TextSelector(), + } +) + +STEP_USER_DATA_SCHEMA_V2_B = vol.Schema( + { + vol.Required(CONF_STATION_ID): TextSelector(), } ) @@ -66,11 +81,13 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, hub = WLHub( username=data["username"], password=data["password"], - apitoken=data["apitoken"], + apitoken=data[CONF_API_TOKEN], websession=websession, ) - if not await hub.authenticate(data["username"], data["password"], data["apitoken"]): + if not await hub.authenticate( + data["username"], data["password"], data[CONF_API_TOKEN] + ): raise InvalidAuth # If you cannot connect: @@ -97,14 +114,14 @@ async def validate_input_v2( websession = async_get_clientsession(hass) hub = WLHubV2( - station_id=data["station_id"], - api_key_v2=data["api_key_v2"], - api_secret=data["api_secret"], + station_id=data.get(CONF_STATION_ID), + api_key_v2=data[CONF_API_KEY_V2], + api_secret=data[CONF_API_SECRET], websession=websession, ) if not await hub.authenticate( - data["station_id"], data["api_key_v2"], data["api_secret"] + data.get(CONF_STATION_ID), data[CONF_API_KEY_V2], data[CONF_API_SECRET] ): raise InvalidAuth @@ -128,6 +145,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 2 + user_data_2 = {} + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -136,22 +155,22 @@ async def async_step_user( if user_input is None: return self.async_show_form(step_id="user", data_schema=data_schema) - if user_input["api_version"] == API_V1: + if user_input[CONF_API_VERSION] == API_V1: return await self.async_step_user_1() - if user_input["api_version"] == API_V2: + if user_input[CONF_API_VERSION] == API_V2: return await self.async_step_user_2() async def async_step_user_1( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Handle the initial step.""" + """Handle the step for API_V1.""" data_schema = STEP_USER_DATA_SCHEMA_V1 if user_input is None: return self.async_show_form(step_id="user_1", data_schema=data_schema) errors = {} - user_input["api_version"] = API_V1 + user_input[CONF_API_VERSION] = API_V1 try: info = await validate_input(self.hass, user_input) except CannotConnect: @@ -172,14 +191,65 @@ async def async_step_user_1( async def async_step_user_2( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Handle the initial step.""" - data_schema = STEP_USER_DATA_SCHEMA_V2 + """Handle the first step for API_V2.""" + data_schema = STEP_USER_DATA_SCHEMA_V2_A if user_input is None: return self.async_show_form(step_id="user_2", data_schema=data_schema) errors = {} - user_input["api_version"] = API_V2 + user_input[CONF_API_VERSION] = API_V2 + try: + await validate_input_v2(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + self.user_data_2 = user_input + return await self.async_step_user_3() + + return self.async_show_form( + step_id="user_2", data_schema=data_schema, errors=errors + ) + + async def async_step_user_3( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the second step for API_V2.""" + data_schema = STEP_USER_DATA_SCHEMA_V2_B + websession = async_get_clientsession(self.hass) + _api = WLHubV2( + api_key_v2=self.user_data_2[CONF_API_KEY_V2], + api_secret=self.user_data_2[CONF_API_SECRET], + websession=websession, + ) + station_list_raw = await _api.get_all_stations() + station_list = [ + SelectOptionDict(value=str(stn[CONF_STATION_ID]), label=stn["station_name"]) + for stn in (station_list_raw["stations"]) + ] + + if user_input is None: + return self.async_show_form( + step_id="user_3", + data_schema=vol.Schema( + { + vol.Required(CONF_STATION_ID): SelectSelector( + SelectSelectorConfig(options=station_list) + ), + } + ), + ) + errors = {} + + user_input[CONF_API_VERSION] = API_V2 + user_input[CONF_API_KEY_V2] = self.user_data_2[CONF_API_KEY_V2] + user_input[CONF_API_SECRET] = self.user_data_2[CONF_API_SECRET] + try: info = await validate_input_v2(self.hass, user_input) except CannotConnect: @@ -190,11 +260,12 @@ async def async_step_user_2( _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - await self.async_set_unique_id(user_input["station_id"]) + await self.async_set_unique_id(user_input[CONF_STATION_ID]) + self._abort_if_unique_id_configured() return self.async_create_entry(title=info["title"], data=user_input) return self.async_show_form( - step_id="user_2", data_schema=data_schema, errors=errors + step_id="user_3", data_schema=data_schema, errors=errors ) diff --git a/custom_components/weatherlink/const.py b/custom_components/weatherlink/const.py index a71b2f5..11a172e 100644 --- a/custom_components/weatherlink/const.py +++ b/custom_components/weatherlink/const.py @@ -3,4 +3,8 @@ DOMAIN = "weatherlink" VERSION = "0.0.16" -CONF_API_TOKEN = "conf_api_token" +CONF_API_VERSION = "api_version" +CONF_API_KEY_V2 = "api_key_v2" +CONF_API_SECRET = "api_secret" +CONF_API_TOKEN = "apitoken" +CONF_STATION_ID = "station_id" diff --git a/custom_components/weatherlink/diagnostics.py b/custom_components/weatherlink/diagnostics.py index 248da71..84b0cf9 100644 --- a/custom_components/weatherlink/diagnostics.py +++ b/custom_components/weatherlink/diagnostics.py @@ -7,14 +7,15 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import CONF_API_TOKEN, DOMAIN +from .const import CONF_API_KEY_V2, CONF_API_SECRET, CONF_API_TOKEN, DOMAIN TO_REDACT = { CONF_PASSWORD, CONF_USERNAME, CONF_API_TOKEN, - "apitoken", - "DID", + CONF_API_SECRET, + CONF_API_KEY_V2, + "user_email", } @@ -25,9 +26,13 @@ async def async_get_config_entry_diagnostics( coordinator: DataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id][ "coordinator" ] + station_data = hass.data[DOMAIN][config_entry.entry_id]["station_data"] + current = hass.data[DOMAIN][config_entry.entry_id]["current"] diagnostics_data = { "info": async_redact_data(config_entry.data, TO_REDACT), + "station_data": async_redact_data(station_data, TO_REDACT), + "current_data": async_redact_data(current, TO_REDACT), "data": async_redact_data(coordinator.data, TO_REDACT), } diff --git a/custom_components/weatherlink/pyweatherlink.py b/custom_components/weatherlink/pyweatherlink.py index 5192174..70df407 100644 --- a/custom_components/weatherlink/pyweatherlink.py +++ b/custom_components/weatherlink/pyweatherlink.py @@ -46,7 +46,7 @@ async def request(self, method, **kwargs) -> ClientResponse: params = { "user": self.username, "pass": self.password, - "apiToken": self.apitoken, + "apitoken": self.apitoken, } params_enc = urllib.parse.urlencode(params, quote_via=urllib.parse.quote) @@ -75,10 +75,10 @@ class WLHubV2: def __init__( self, - station_id: str, api_key_v2: str, api_secret: str, websession: ClientSession, + station_id: str | None = None, ) -> None: """Initialize.""" self.station_id = station_id @@ -105,15 +105,18 @@ async def request(self, method, endpoint="current/", **kwargs) -> ClientResponse headers["x-api-secret"] = self.api_secret headers["User-Agent"] = f"Weatherlink for Home Assistant/{VERSION}" params = { - # "station_id": self.station_id, "api-key": self.api_key_v2, - # "api_secret": self.api_secret, } params_enc = urllib.parse.urlencode(params, quote_via=urllib.parse.quote) + if self.station_id is None: + station = "" + else: + station = f"{self.station_id}" + res = await self.websession.request( method, - f"{API_V2_URL}{endpoint}{self.station_id}?{params_enc}", + f"{API_V2_URL}{endpoint}{station}?{params_enc}", **kwargs, headers=headers, ) @@ -140,6 +143,16 @@ async def get_station(self): "API get_station failed. Status: %s, - %s", exc.code, exc.message ) + async def get_all_stations(self): + """Get all stations from api.""" + try: + res = await self.request("GET", endpoint="stations") + return await res.json() + except ClientResponseError as exc: + _LOGGER.error( + "API get_all_stations failed. Status: %s, - %s", exc.code, exc.message + ) + @dataclass class WLData: diff --git a/custom_components/weatherlink/sensor.py b/custom_components/weatherlink/sensor.py index eeae7e6..13ab03c 100644 --- a/custom_components/weatherlink/sensor.py +++ b/custom_components/weatherlink/sensor.py @@ -28,7 +28,7 @@ from . import get_coordinator from .config_flow import API_V1, API_V2 -from .const import DOMAIN +from .const import CONF_API_VERSION, DOMAIN from .pyweatherlink import WLData _LOGGER = logging.getLogger(__name__) @@ -195,25 +195,34 @@ def __init__( self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self.get_unique_id_base())}, name=self.generate_name(), - manufacturer="Davis", + manufacturer="Davis Instruments", model=self.generate_model(), + sw_version=self.get_firmware(), configuration_url="https://www.weatherlink.com/", ) def get_unique_id_base(self): """Generate base for unique_id.""" unique_base = None - if self.entry.data["api_version"] == API_V1: + if self.entry.data[CONF_API_VERSION] == API_V1: unique_base = self.coordinator.data["DID"] - if self.entry.data["api_version"] == API_V2: + if self.entry.data[CONF_API_VERSION] == API_V2: unique_base = self.coordinator.data["station_id_uuid"] return unique_base + def get_firmware(self) -> str | None: + """Get firmware version.""" + if self.entry.data[CONF_API_VERSION] == API_V2: + return self.hass.data[DOMAIN][self.entry.entry_id]["station_data"][ + "stations" + ][0].get("firmware_version") + return None + def generate_name(self): """Generate device name.""" - if self.entry.data["api_version"] == API_V1: + if self.entry.data[CONF_API_VERSION] == API_V1: return self.coordinator.data["station_name"] - if self.entry.data["api_version"] == API_V2: + if self.entry.data[CONF_API_VERSION] == API_V2: return self.hass.data[DOMAIN][self.entry.entry_id]["station_data"][ "stations" ][0]["station_name"] @@ -222,14 +231,19 @@ def generate_name(self): def generate_model(self): """Generate model string.""" - if self.entry.data["api_version"] == API_V1: + if self.entry.data[CONF_API_VERSION] == API_V1: return "Weatherlink - API V1" - if self.entry.data["api_version"] == API_V2: - model = self.hass.data[DOMAIN][self.entry.entry_id]["station_data"][ + if self.entry.data[CONF_API_VERSION] == API_V2: + model: str = self.hass.data[DOMAIN][self.entry.entry_id]["station_data"][ "stations" ][0].get("product_number") - return f"Weatherlink {model}" - return "Weatherlink" + if model == "6555": + return f"WeatherLinkIP {model}" + if model.startswith("6100"): + return f"WeatherLink Live {model}" + if model.startswith("6313"): + return f"WeatherLink Console {model}" + return "WeatherLink" @property def native_value(self): diff --git a/custom_components/weatherlink/translations/en.json b/custom_components/weatherlink/translations/en.json index 889f97b..dceeb85 100644 --- a/custom_components/weatherlink/translations/en.json +++ b/custom_components/weatherlink/translations/en.json @@ -39,6 +39,9 @@ "data_description": { "station_id": "Both integer or UUID station id types are accepted." } + }, + "user_3": { + "description": "Select weather station" } } }, diff --git a/custom_components/weatherlink/translations/sv.json b/custom_components/weatherlink/translations/sv.json index ec6bd70..8b3fd39 100644 --- a/custom_components/weatherlink/translations/sv.json +++ b/custom_components/weatherlink/translations/sv.json @@ -39,6 +39,9 @@ "data_description": { "station_id": "Stations-id kan anges som heltal eller UUID." } + }, + "user_3": { + "description": "Välj väderstation" } } },