Skip to content

Commit

Permalink
more optional values, flatten conditions
Browse files Browse the repository at this point in the history
  • Loading branch information
siku2 committed May 22, 2021
1 parent a4b4c03 commit 5e595b6
Show file tree
Hide file tree
Showing 5 changed files with 285 additions and 118 deletions.
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,21 @@ A custom component for Davis Instruments' [WeatherLink](https://www.davisinstrum
2. Install the "WeatherLink" integration in HACS
3. Head over to the Home Assistant configuration and set up the integration there

## AirLink
## Limitations

WeatherLink groups data into multiple data structures. For instance, all the data reported by the ISS outdoor station (temperature, wind, rain, solar, etc.) is reported in a single data structure.
When the integration polls the API, it receives a list of these data structures.
It's possible for WeatherLink to report multiple instances of the same data structure. This happens, for instance, when you physically separate parts of the ISS and use multiple channels.
This integration doesn't handle this case very well. If it receives multiple instances of the same data structure,
it primarily uses the first one and only uses the subsequent ones to fill holes in the first one.
If you're running into a problem that's caused by this behaviour, please open an issue.

### AirLink

Older versions of the AirLink firmware use a different data structure format when transmitting updates.
This integration currently doesn't support this so in case AirLink isn't working, try updating the firmware.

### AQI
#### AQI

The calculation of AQI varies from country to country and may require inputs that are not available to a single sensor.
For AQI calculations using the latest AQI models.
63 changes: 43 additions & 20 deletions custom_components/weatherlink/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,51 +98,51 @@ class IssCondition(ConditionRecord):
rx_state: Optional[ReceiverState]
"""configured radio receiver state"""

temp: float
temp: Optional[float]
"""most recent valid temperature"""
hum: float
hum: Optional[float]
"""most recent valid humidity **(%RH)**"""
dew_point: float
dew_point: Optional[float]
""""""
wet_bulb: Optional[float]
""""""
heat_index: float
heat_index: Optional[float]
""""""
wind_chill: Optional[float]
""""""
thw_index: float
thw_index: Optional[float]
""""""
thsw_index: float
thsw_index: Optional[float]
""""""

wind_speed_last: float
wind_speed_last: Optional[float]
"""most recent valid wind speed **(km/h)**"""
wind_dir_last: Optional[int]
"""most recent valid wind direction **(°degree)**"""

wind_speed_avg_last_1_min: float
wind_speed_avg_last_1_min: Optional[float]
"""average wind speed over last 1 min **(km/h)**"""
wind_dir_scalar_avg_last_1_min: int
wind_dir_scalar_avg_last_1_min: Optional[int]
"""scalar average wind direction over last 1 min **(°degree)**"""

wind_speed_avg_last_2_min: float
wind_speed_avg_last_2_min: Optional[float]
"""average wind speed over last 2 min **(km/h)**"""
wind_dir_scalar_avg_last_2_min: float
wind_dir_scalar_avg_last_2_min: Optional[int]
"""scalar average wind direction over last 2 min **(°degree)**"""

wind_speed_hi_last_2_min: Optional[float]
"""maximum wind speed over last 2 min **(km/h)**"""
wind_dir_at_hi_speed_last_2_min: Optional[float]
wind_dir_at_hi_speed_last_2_min: Optional[int]
"""gust wind direction over last 2 min **(°degree)**"""

wind_speed_avg_last_10_min: float
wind_speed_avg_last_10_min: Optional[float]
"""average wind speed over last 10 min **(km/h)**"""
wind_dir_scalar_avg_last_10_min: Optional[float]
wind_dir_scalar_avg_last_10_min: Optional[int]
"""scalar average wind direction over last 10 min **(°degree)**"""

wind_speed_hi_last_10_min: float
wind_speed_hi_last_10_min: Optional[float]
"""maximum wind speed over last 10 min **(km/h)**"""
wind_dir_at_hi_speed_last_10_min: float
wind_dir_at_hi_speed_last_10_min: Optional[int]
"""gust wind direction over last 10 min **(°degree)**"""

rain_size: CollectorSize
Expand Down Expand Up @@ -175,9 +175,9 @@ class IssCondition(ConditionRecord):
rain_storm_start_at: Optional[datetime]
"""timestamp of current rain storm start"""

solar_rad: int
solar_rad: Optional[int]
"""most recent solar radiation **(W/m²)**"""
uv_index: float
uv_index: Optional[float]
"""most recent UV index **(Index)**"""

trans_battery_flag: int
Expand Down Expand Up @@ -384,6 +384,8 @@ def _from_json(cls, data: JsonObject, **kwargs):
return cls(**data)


_STRUCTURE_TYPE_KEY = "data_structure_type"

_COND2CLS = {
ConditionType.Iss: IssCondition,
ConditionType.Moisture: MoistureCondition,
Expand All @@ -394,11 +396,25 @@ def _from_json(cls, data: JsonObject, **kwargs):


def condition_from_json(data: JsonObject, **kwargs) -> ConditionRecord:
cond_ty = ConditionType(data.pop("data_structure_type"))
cond_ty = ConditionType(data.pop(_STRUCTURE_TYPE_KEY))
cls = cond_ty.record_class()
return cls.from_json(data, **kwargs)


def flatten_conditions(conditions: Iterable[JsonObject]) -> List[JsonObject]:
cond_by_type = {}
for cond in conditions:
cond_type = cond[_STRUCTURE_TYPE_KEY]
try:
existing = cond_by_type[cond_type]
except KeyError:
cond_by_type[cond_type] = cond
else:
update_dict_where_none(existing, cond)

return list(cond_by_type.values())


class DeviceType(enum.Enum):
WeatherLink = "WeatherLink"
AirLink = "AirLink"
Expand Down Expand Up @@ -426,7 +442,8 @@ class CurrentConditions(FromJson, Mapping[Type[RecordT], RecordT]):
@classmethod
def _from_json(cls, data: JsonObject, **kwargs):
conditions = []
for i, cond_data in enumerate(data["conditions"]):
raw_conditions = flatten_conditions(data["conditions"])
for i, cond_data in enumerate(raw_conditions):
try:
cond = condition_from_json(cond_data, **kwargs)
except Exception:
Expand Down Expand Up @@ -576,3 +593,9 @@ def json_set_default_none(d: JsonObject, *keys: str) -> None:
for key in keys:
if key not in d:
d[key] = None


def update_dict_where_none(d: JsonObject, updates: JsonObject) -> None:
for key, value in updates.items():
if d.get(key) is None:
d[key] = value
34 changes: 19 additions & 15 deletions custom_components/weatherlink/sensor_iss.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,18 +78,18 @@ class Temperature(
):
@property
def state(self):
return round(self._iss_condition.temp, DECIMALS_TEMPERATURE)
return round_optional(self._iss_condition.temp, DECIMALS_TEMPERATURE)

@property
def device_state_attributes(self):
c = self._iss_condition
return {
"dew_point": round(c.dew_point, DECIMALS_TEMPERATURE),
"dew_point": round_optional(c.dew_point, DECIMALS_TEMPERATURE),
"wet_bulb": round_optional(c.wet_bulb, DECIMALS_TEMPERATURE),
"heat_index": round(c.heat_index, DECIMALS_TEMPERATURE),
"heat_index": round_optional(c.heat_index, DECIMALS_TEMPERATURE),
"wind_chill": round_optional(c.wind_chill, DECIMALS_TEMPERATURE),
"thw_index": round(c.thw_index, DECIMALS_TEMPERATURE),
"thsw_index": round(c.thsw_index, DECIMALS_TEMPERATURE),
"thw_index": round_optional(c.thw_index, DECIMALS_TEMPERATURE),
"thsw_index": round_optional(c.thsw_index, DECIMALS_TEMPERATURE),
}


Expand All @@ -101,7 +101,7 @@ class ThswIndex(
):
@property
def state(self):
return round(self._iss_condition.thsw_index, DECIMALS_TEMPERATURE)
return round_optional(self._iss_condition.thsw_index, DECIMALS_TEMPERATURE)


class Humidity(
Expand All @@ -112,7 +112,7 @@ class Humidity(
):
@property
def state(self):
return round(self._iss_condition.hum, DECIMALS_HUMIDITY)
return round_optional(self._iss_condition.hum, DECIMALS_HUMIDITY)


class WindSpeed(
Expand All @@ -127,13 +127,15 @@ def icon(self):

@property
def state(self):
return round(self._iss_condition.wind_speed_avg_last_2_min, DECIMALS_SPEED)
return round_optional(
self._iss_condition.wind_speed_avg_last_2_min, DECIMALS_SPEED
)

@property
def device_state_attributes(self):
c = self._iss_condition
return {
"10_min": round(c.wind_speed_avg_last_10_min, DECIMALS_SPEED),
"10_min": round_optional(c.wind_speed_avg_last_10_min, DECIMALS_SPEED),
}


Expand All @@ -157,7 +159,7 @@ def state(self):
def device_state_attributes(self):
c = self._iss_condition
return {
"10_min": round(c.wind_speed_hi_last_10_min, DECIMALS_SPEED),
"10_min": round_optional(c.wind_speed_hi_last_10_min, DECIMALS_SPEED),
}


Expand All @@ -173,19 +175,21 @@ def icon(self):

@property
def state(self):
return round(
return round_optional(
self._iss_condition.wind_dir_scalar_avg_last_2_min, DECIMALS_DIRECTION
)

@property
def device_state_attributes(self):
c = self._iss_condition
return {
"high": round_optional(c.wind_dir_at_hi_speed_last_2_min, DECIMALS_DIRECTION),
"high": round_optional(
c.wind_dir_at_hi_speed_last_2_min, DECIMALS_DIRECTION
),
"10_min": round_optional(
c.wind_dir_scalar_avg_last_10_min, DECIMALS_DIRECTION
),
"10_min_high": round(
"10_min_high": round_optional(
c.wind_dir_at_hi_speed_last_10_min, DECIMALS_DIRECTION
),
}
Expand All @@ -203,7 +207,7 @@ def icon(self):

@property
def state(self):
return round(self._iss_condition.solar_rad, DECIMALS_RADIATION)
return round_optional(self._iss_condition.solar_rad, DECIMALS_RADIATION)


class UvIndex(
Expand All @@ -218,7 +222,7 @@ def icon(self):

@property
def state(self):
return round(self._iss_condition.uv_index, DECIMALS_UV)
return round_optional(self._iss_condition.uv_index, DECIMALS_UV)


class RainRate(
Expand Down
20 changes: 12 additions & 8 deletions custom_components/weatherlink/weather.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from . import WeatherLinkCoordinator, WeatherLinkEntity
from .api import IssCondition, LssBarCondition
from .const import DECIMALS_DIRECTION, DECIMALS_PRESSURE, DECIMALS_SPEED, DOMAIN
from .sensor_common import round_optional

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -49,11 +50,13 @@ def humidity(self):

@property
def wind_speed(self):
return round(self._iss_condition.wind_speed_avg_last_2_min, DECIMALS_SPEED)
return round_optional(
self._iss_condition.wind_speed_avg_last_2_min, DECIMALS_SPEED
)

@property
def wind_bearing(self):
return round(
return round_optional(
self._iss_condition.wind_dir_scalar_avg_last_2_min, DECIMALS_DIRECTION
)

Expand All @@ -63,24 +66,25 @@ def condition(self):

rain_rate = c.rain_rate_hi or 0.0
if rain_rate > 0.25:
if c.temp <= 0:
return "snowy"
elif 0 < c.temp < 5:
return "snowy-rainy"
if temp := c.temp:
if temp <= 0:
return "snowy"
elif 0 < temp < 5:
return "snowy-rainy"

if rain_rate > 4.0:
return "pouring"

return "rainy"

if c.wind_speed_avg_last_2_min > 20:
if (c.wind_speed_avg_last_2_min or 0.0) > 20:
return "windy"

if state := self.hass.states.get("sun.sun"):
if state.state == "below_horizon":
return "clear-night"

if c.solar_rad > 500:
if (c.solar_rad or 0) > 500:
return "sunny"

return "partlycloudy"
Loading

0 comments on commit 5e595b6

Please sign in to comment.