From 01859a5aa2460e468dc0962ed85f2e40cbe5ffb8 Mon Sep 17 00:00:00 2001 From: garbled1 Date: Fri, 13 Nov 2020 07:43:18 -0700 Subject: [PATCH 01/28] Step 1, break things out into a const.py --- custom_components/ecowitt/__init__.py | 533 ++------------------------ custom_components/ecowitt/const.py | 515 +++++++++++++++++++++++++ 2 files changed, 539 insertions(+), 509 deletions(-) create mode 100644 custom_components/ecowitt/const.py diff --git a/custom_components/ecowitt/__init__.py b/custom_components/ecowitt/__init__.py index b0abc4a..7e689ab 100644 --- a/custom_components/ecowitt/__init__.py +++ b/custom_components/ecowitt/__init__.py @@ -1,5 +1,5 @@ """The Ecowitt Weather Station Component.""" -import asyncio +# import asyncio import logging import time @@ -21,523 +21,38 @@ from homeassistant.helpers.entity import Entity from homeassistant.const import ( - DEGREE, CONF_PORT, CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL, - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - POWER_WATT, - TEMP_CELSIUS, - PERCENTAGE, - PRESSURE_HPA, - PRESSURE_INHG, - LENGTH_INCHES, - LENGTH_KILOMETERS, - LENGTH_MILES, - SPEED_KILOMETERS_PER_HOUR, - SPEED_MILES_PER_HOUR, - TIME_HOURS, - TIME_DAYS, - TIME_WEEKS, - TIME_MONTHS, - TIME_YEARS, - UV_INDEX, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TIMESTAMP, ) -from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOISTURE, -) - -_LOGGER = logging.getLogger(__name__) - -TYPE_SENSOR = "sensor" -TYPE_BINARY_SENSOR = "binary_sensor" -DOMAIN = "ecowitt" -DATA_CONFIG = "config" -DATA_ECOWITT = "ecowitt_listener" -DATA_STATION = "station" -DATA_PASSKEY = "PASSKEY" -DATA_STATIONTYPE = "stationtype" -DATA_FREQ = "freq" -DATA_MODEL = "model" -DATA_READY = "ready" - -CONF_UNIT_BARO = "barounit" -CONF_UNIT_WIND = "windunit" -CONF_UNIT_RAIN = "rainunit" -CONF_UNIT_WINDCHILL = "windchillunit" -CONF_UNIT_LIGHTNING = "lightningunit" - -TYPE_BAROMABSHPA = "baromabshpa" -TYPE_BAROMRELHPA = "baromrelhpa" -TYPE_BAROMABSIN = "baromabsin" -TYPE_BAROMRELIN = "baromrelin" -TYPE_RAINRATEIN = "rainratein" -TYPE_EVENTRAININ = "eventrainin" -TYPE_HOURLYRAININ = "hourlyrainin" -TYPE_TOTALRAININ = "totalrainin" -TYPE_DAILYRAININ = "dailyrainin" -TYPE_WEEKLYRAININ = "weeklyrainin" -TYPE_MONTHLYRAININ = "monthlyrainin" -TYPE_YEARLYRAININ = "yearlyrainin" -TYPE_RAINRATEMM = "rainratemm" -TYPE_EVENTRAINMM = "eventrainmm" -TYPE_HOURLYRAINMM = "hourlyrainmm" -TYPE_TOTALRAINMM = "totalrainmm" -TYPE_DAILYRAINMM = "dailyrainmm" -TYPE_WEEKLYRAINMM = "weeklyrainmm" -TYPE_MONTHLYRAINMM = "monthlyrainmm" -TYPE_YEARLYRAINMM = "yearlyrainmm" -TYPE_HUMIDITY = "humidity" -TYPE_HUMIDITY1 = "humidity1" -TYPE_HUMIDITY2 = "humidity2" -TYPE_HUMIDITY3 = "humidity3" -TYPE_HUMIDITY4 = "humidity4" -TYPE_HUMIDITY5 = "humidity5" -TYPE_HUMIDITY6 = "humidity6" -TYPE_HUMIDITY7 = "humidity7" -TYPE_HUMIDITY8 = "humidity8" -TYPE_HUMIDITYIN = "humidityin" -TYPE_WINDDIR = "winddir" -TYPE_WINDDIR_A10 = "winddir_avg10m" -TYPE_WINDSPEEDKMH = "windspeedkmh" -TYPE_WINDSPEEDKMH_A10 = "windspdkmh_avg10m" -TYPE_WINDGUSTKMH = "windgustkmh" -TYPE_WINDSPEEDMPH = "windspeedmph" -TYPE_WINDSPEEDMPH_A10 = "windspdmph_avg10m" -TYPE_WINDGUSTMPH = "windgustmph" -TYPE_MAXDAILYGUST = "maxdailygust" -TYPE_MAXDAILYGUSTKMH = "maxdailygustkmh" -TYPE_TEMPC = "tempc" -TYPE_TEMPINC = "tempinc" -TYPE_TEMP1C = "temp1c" -TYPE_TEMP2C = "temp2c" -TYPE_TEMP3C = "temp3c" -TYPE_TEMP4C = "temp4c" -TYPE_TEMP5C = "temp5c" -TYPE_TEMP6C = "temp6c" -TYPE_TEMP7C = "temp7c" -TYPE_TEMP8C = "temp8c" -TYPE_DEWPOINTC = "dewpointc" -TYPE_DEWPOINTINC = "dewpointinc" -TYPE_DEWPOINT1C = "dewpoint1c" -TYPE_DEWPOINT2C = "dewpoint2c" -TYPE_DEWPOINT3C = "dewpoint3c" -TYPE_DEWPOINT4C = "dewpoint4c" -TYPE_DEWPOINT5C = "dewpoint5c" -TYPE_DEWPOINT6C = "dewpoint6c" -TYPE_DEWPOINT7C = "dewpoint7c" -TYPE_DEWPOINT8C = "dewpoint8c" -TYPE_WINDCHILLC = "windchillc" -TYPE_SOLARRADIATION = "solarradiation" -TYPE_UV = "uv" -TYPE_SOILMOISTURE1 = "soilmoisture1" -TYPE_SOILMOISTURE2 = "soilmoisture2" -TYPE_SOILMOISTURE3 = "soilmoisture3" -TYPE_SOILMOISTURE4 = "soilmoisture4" -TYPE_SOILMOISTURE5 = "soilmoisture5" -TYPE_SOILMOISTURE6 = "soilmoisture6" -TYPE_SOILMOISTURE7 = "soilmoisture7" -TYPE_SOILMOISTURE8 = "soilmoisture8" -TYPE_PM25_CH1 = "pm25_ch1" -TYPE_PM25_CH2 = "pm25_ch2" -TYPE_PM25_CH3 = "pm25_ch3" -TYPE_PM25_CH4 = "pm25_ch4" -TYPE_PM25_AVG_24H_CH1 = "pm25_avg_24h_ch1" -TYPE_PM25_AVG_24H_CH2 = "pm25_avg_24h_ch2" -TYPE_PM25_AVG_24H_CH3 = "pm25_avg_24h_ch3" -TYPE_PM25_AVG_24H_CH4 = "pm25_avg_24h_ch4" -TYPE_LIGHTNING_TIME = "lightning_time" -TYPE_LIGHTNING_NUM = "lightning_num" -TYPE_LIGHTNING_KM = "lightning" -TYPE_LIGHTNING_MI = "lightning_mi" -TYPE_LEAK_CH1 = "leak_ch1" -TYPE_LEAK_CH2 = "leak_ch2" -TYPE_LEAK_CH3 = "leak_ch3" -TYPE_LEAK_CH4 = "leak_ch4" -TYPE_WH25BATT = "wh25batt" -TYPE_WH26BATT = "wh26batt" -TYPE_WH40BATT = "wh40batt" -TYPE_WH57BATT = "wh57batt" -TYPE_WH68BATT = "wh68batt" -TYPE_WH65BATT = "wh65batt" -TYPE_WH80BATT = "wh80batt" -TYPE_SOILBATT1 = "soilbatt1" -TYPE_SOILBATT2 = "soilbatt2" -TYPE_SOILBATT3 = "soilbatt3" -TYPE_SOILBATT4 = "soilbatt4" -TYPE_SOILBATT5 = "soilbatt5" -TYPE_SOILBATT6 = "soilbatt6" -TYPE_SOILBATT7 = "soilbatt7" -TYPE_SOILBATT8 = "soilbatt8" -TYPE_BATTERY1 = "batt1" -TYPE_BATTERY2 = "batt2" -TYPE_BATTERY3 = "batt3" -TYPE_BATTERY4 = "batt4" -TYPE_BATTERY5 = "batt5" -TYPE_BATTERY6 = "batt6" -TYPE_BATTERY7 = "batt7" -TYPE_BATTERY8 = "batt8" -TYPE_PM25BATT1 = "pm25batt1" -TYPE_PM25BATT2 = "pm25batt2" -TYPE_PM25BATT3 = "pm25batt3" -TYPE_PM25BATT4 = "pm25batt4" -TYPE_PM25BATT5 = "pm25batt5" -TYPE_PM25BATT6 = "pm25batt6" -TYPE_PM25BATT7 = "pm25batt7" -TYPE_PM25BATT8 = "pm25batt8" -TYPE_LEAKBATT1 = "leakbatt1" -TYPE_LEAKBATT2 = "leakbatt2" -TYPE_LEAKBATT3 = "leakbatt3" -TYPE_LEAKBATT4 = "leakbatt4" -TYPE_LEAKBATT5 = "leakbatt5" -TYPE_LEAKBATT6 = "leakbatt6" -TYPE_LEAKBATT7 = "leakbatt7" -TYPE_LEAKBATT8 = "leakbatt8" - -S_METRIC = 1 -S_IMPERIAL = 2 - -W_TYPE_NEW = "new" -W_TYPE_OLD = "old" -W_TYPE_HYBRID = "hybrid" - -LEAK_DETECTED = "Leak Detected" - -# Name, unit_of_measure, type, device_class, icon, metric=1 -# name, uom, kind, device_class, icon, metric = SENSOR_TYPES[x] -SENSOR_TYPES = { - TYPE_BAROMABSHPA: ("Absolute Pressure", PRESSURE_HPA, - TYPE_SENSOR, DEVICE_CLASS_PRESSURE, - "mdi:gauge", S_METRIC), - TYPE_BAROMRELHPA: ("Relative Pressure", PRESSURE_HPA, - TYPE_SENSOR, DEVICE_CLASS_PRESSURE, - "mdi:gauge", S_METRIC), - TYPE_BAROMABSIN: ("Absolute Pressure", PRESSURE_INHG, - TYPE_SENSOR, DEVICE_CLASS_PRESSURE, - "mdi:gauge", S_IMPERIAL), - TYPE_BAROMRELIN: ("Relative Pressure", PRESSURE_INHG, - TYPE_SENSOR, DEVICE_CLASS_PRESSURE, - "mdi:gauge", S_IMPERIAL), - TYPE_RAINRATEIN: ("Rain Rate", f"{LENGTH_INCHES}/{TIME_HOURS}", - TYPE_SENSOR, None, "mdi:water", S_IMPERIAL), - TYPE_EVENTRAININ: ("Event Rain Rate", f"{LENGTH_INCHES}/{TIME_HOURS}", - TYPE_SENSOR, None, "mdi:water", S_IMPERIAL), - TYPE_HOURLYRAININ: ("Hourly Rain Rate", f"{LENGTH_INCHES}/{TIME_HOURS}", - TYPE_SENSOR, None, "mdi:water", S_IMPERIAL), - TYPE_TOTALRAININ: ("Total Rain Rate", f"{LENGTH_INCHES}", - TYPE_SENSOR, None, "mdi:water", S_IMPERIAL), - TYPE_DAILYRAININ: ("Daily Rain Rate", f"{LENGTH_INCHES}/{TIME_DAYS}", - TYPE_SENSOR, None, "mdi:water", S_IMPERIAL), - TYPE_WEEKLYRAININ: ("Weekly Rain Rate", f"{LENGTH_INCHES}/{TIME_WEEKS}", - TYPE_SENSOR, None, "mdi:water", S_IMPERIAL), - TYPE_MONTHLYRAININ: ("Monthly Rain Rate", f"{LENGTH_INCHES}/{TIME_MONTHS}", - TYPE_SENSOR, None, "mdi:water", S_IMPERIAL), - TYPE_YEARLYRAININ: ("Yearly Rain Rate", f"{LENGTH_INCHES}/{TIME_YEARS}", - TYPE_SENSOR, None, "mdi:water", S_IMPERIAL), - TYPE_RAINRATEMM: ("Rain Rate", f"mm/{TIME_HOURS}", - TYPE_SENSOR, None, "mdi:water", S_METRIC), - TYPE_EVENTRAINMM: ("Event Rain Rate", f"mm/{TIME_HOURS}", - TYPE_SENSOR, None, "mdi:water", S_METRIC), - TYPE_HOURLYRAINMM: ("Hourly Rain Rate", f"mm/{TIME_HOURS}", - TYPE_SENSOR, None, "mdi:water", S_METRIC), - TYPE_TOTALRAINMM: ("Total Rain Rate", f"mm", - TYPE_SENSOR, None, "mdi:water", S_METRIC), - TYPE_DAILYRAINMM: ("Daily Rain Rate", f"mm/{TIME_DAYS}", - TYPE_SENSOR, None, "mdi:water", S_METRIC), - TYPE_WEEKLYRAINMM: ("Weekly Rain Rate", f"mm/{TIME_WEEKS}", - TYPE_SENSOR, None, "mdi:water", S_METRIC), - TYPE_MONTHLYRAINMM: ("Monthly Rain Rate", f"mm/{TIME_MONTHS}", - TYPE_SENSOR, None, "mdi:water", S_METRIC), - TYPE_YEARLYRAINMM: ("Yearly Rain Rate", f"mm/{TIME_YEARS}", - TYPE_SENSOR, None, "mdi:water", S_METRIC), - TYPE_HUMIDITY: ("Humidity", PERCENTAGE, - TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, - "mdi:water-percent", 0), - TYPE_HUMIDITYIN: ("Indoor Humidity", PERCENTAGE, - TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, - "mdi:water-percent", 0), - TYPE_HUMIDITY1: ("Humidity 1", PERCENTAGE, - TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, - "mdi:water-percent", 0), - TYPE_HUMIDITY2: ("Humidity 2", PERCENTAGE, - TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, - "mdi:water-percent", 0), - TYPE_HUMIDITY3: ("Humidity 3", PERCENTAGE, - TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, - "mdi:water-percent", 0), - TYPE_HUMIDITY4: ("Humidity 4", PERCENTAGE, - TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, - "mdi:water-percent", 0), - TYPE_HUMIDITY5: ("Humidity 5", PERCENTAGE, - TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, - "mdi:water-percent", 0), - TYPE_HUMIDITY6: ("Humidity 6", PERCENTAGE, - TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, - "mdi:water-percent", 0), - TYPE_HUMIDITY7: ("Humidity 7", PERCENTAGE, - TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, - "mdi:water-percent", 0), - TYPE_HUMIDITY8: ("Humidity 8", PERCENTAGE, - TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, - "mdi:water-percent", 0), - TYPE_WINDDIR: ("Wind Direction", DEGREE, - TYPE_SENSOR, None, "mdi:water-percent", 0), - TYPE_WINDDIR_A10: ("Wind Direction 10m Avg", DEGREE, - TYPE_SENSOR, None, "mdi:water-percent", 0), - TYPE_WINDSPEEDKMH: ("Wind Speed", SPEED_KILOMETERS_PER_HOUR, - TYPE_SENSOR, None, "mdi:weather-windy", S_METRIC), - TYPE_WINDSPEEDKMH_A10: ("Wind Speed 10m Avg", SPEED_KILOMETERS_PER_HOUR, - TYPE_SENSOR, None, "mdi:weather-windy", S_METRIC), - TYPE_WINDGUSTKMH: ("Wind Gust", SPEED_KILOMETERS_PER_HOUR, - TYPE_SENSOR, None, "mdi:weather-windy", S_METRIC), - TYPE_WINDSPEEDMPH: ("Wind Speed", SPEED_MILES_PER_HOUR, - TYPE_SENSOR, None, "mdi:weather-windy", S_IMPERIAL), - TYPE_WINDSPEEDMPH_A10: ("Wind Speed 10m Avg", SPEED_MILES_PER_HOUR, - TYPE_SENSOR, None, "mdi:weather-windy", - S_IMPERIAL), - TYPE_WINDGUSTMPH: ("Wind Gust", SPEED_MILES_PER_HOUR, - TYPE_SENSOR, None, "mdi:weather-windy", S_IMPERIAL), - TYPE_MAXDAILYGUST: ("Max Daily Wind Gust", SPEED_MILES_PER_HOUR, - TYPE_SENSOR, None, "mdi:weather-windy", S_IMPERIAL), - TYPE_MAXDAILYGUSTKMH: ("Max Daily Wind Gust", SPEED_KILOMETERS_PER_HOUR, - TYPE_SENSOR, None, "mdi:weather-windy", S_METRIC), - TYPE_TEMPC: ("Outdoor Temperature", TEMP_CELSIUS, - TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, "mdi:thermometer", 0), - TYPE_TEMP1C: ("Temperature 1", TEMP_CELSIUS, - TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, "mdi:thermometer", 0), - TYPE_TEMP2C: ("Temperature 2", TEMP_CELSIUS, - TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, "mdi:thermometer", 0), - TYPE_TEMP3C: ("Temperature 3", TEMP_CELSIUS, - TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, "mdi:thermometer", 0), - TYPE_TEMP4C: ("Temperature 4", TEMP_CELSIUS, - TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, "mdi:thermometer", 0), - TYPE_TEMP5C: ("Temperature 5", TEMP_CELSIUS, - TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, "mdi:thermometer", 0), - TYPE_TEMP6C: ("Temperature 6", TEMP_CELSIUS, - TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, "mdi:thermometer", 0), - TYPE_TEMP7C: ("Temperature 7", TEMP_CELSIUS, - TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, "mdi:thermometer", 0), - TYPE_TEMP8C: ("Temperature 8", TEMP_CELSIUS, - TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, "mdi:thermometer", 0), - TYPE_TEMPINC: ("Indoor Temperature", TEMP_CELSIUS, - TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, - "mdi:thermometer", 0), - TYPE_DEWPOINTC: ("Dewpoint", TEMP_CELSIUS, - TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, - "mdi:thermometer", 0), - TYPE_DEWPOINTINC: ("Indoor Dewpoint", TEMP_CELSIUS, - TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, - "mdi:thermometer", 0), - TYPE_DEWPOINT1C: ("Dewpoint 1", TEMP_CELSIUS, - TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, - "mdi:thermometer", 0), - TYPE_DEWPOINT2C: ("Dewpoint 2", TEMP_CELSIUS, - TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, - "mdi:thermometer", 0), - TYPE_DEWPOINT3C: ("Dewpoint 3", TEMP_CELSIUS, - TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, - "mdi:thermometer", 0), - TYPE_DEWPOINT4C: ("Dewpoint 4", TEMP_CELSIUS, - TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, - "mdi:thermometer", 0), - TYPE_DEWPOINT5C: ("Dewpoint 5", TEMP_CELSIUS, - TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, - "mdi:thermometer", 0), - TYPE_DEWPOINT6C: ("Dewpoint 6", TEMP_CELSIUS, - TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, - "mdi:thermometer", 0), - TYPE_DEWPOINT7C: ("Dewpoint 7", TEMP_CELSIUS, - TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, - "mdi:thermometer", 0), - TYPE_DEWPOINT8C: ("Dewpoint 8", TEMP_CELSIUS, - TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, - "mdi:thermometer", 0), - TYPE_WINDCHILLC: ("Windchill", TEMP_CELSIUS, - TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, - "mdi:thermometer", 0), - TYPE_SOLARRADIATION: ("Solar Radiation", f"{POWER_WATT}/m^2", - TYPE_SENSOR, DEVICE_CLASS_ILLUMINANCE, - "mdi:weather-sunny", 0), - TYPE_UV: ("UV Index", UV_INDEX, - TYPE_SENSOR, None, "mdi:sunglasses", 0), - TYPE_SOILMOISTURE1: ("Soil Moisture 1", PERCENTAGE, - TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, - "mdi:water-percent", 0), - TYPE_SOILMOISTURE2: ("Soil Moisture 2", PERCENTAGE, - TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, - "mdi:water-percent", 0), - TYPE_SOILMOISTURE3: ("Soil Moisture 3", PERCENTAGE, - TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, - "mdi:water-percent", 0), - TYPE_SOILMOISTURE4: ("Soil Moisture 4", PERCENTAGE, - TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, - "mdi:water-percent", 0), - TYPE_SOILMOISTURE5: ("Soil Moisture 5", PERCENTAGE, - TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, - "mdi:water-percent", 0), - TYPE_SOILMOISTURE6: ("Soil Moisture 6", PERCENTAGE, - TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, - "mdi:water-percent", 0), - TYPE_SOILMOISTURE7: ("Soil Moisture 7", PERCENTAGE, - TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, - "mdi:water-percent", 0), - TYPE_SOILMOISTURE8: ("Soil Moisture 8", PERCENTAGE, - TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, - "mdi:water-percent", 0), - TYPE_PM25_CH1: ("PM2.5 1", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - TYPE_SENSOR, None, "mdi:eye", 0), - TYPE_PM25_CH2: ("PM2.5 2", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - TYPE_SENSOR, None, "mdi:eye", 0), - TYPE_PM25_CH3: ("PM2.5 3", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - TYPE_SENSOR, None, "mdi:eye", 0), - TYPE_PM25_CH4: ("PM2.5 4", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - TYPE_SENSOR, None, "mdi:eye", 0), - TYPE_PM25_AVG_24H_CH1: ("PM2.5 24h average 1", - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - TYPE_SENSOR, None, "mdi:eye", 0), - TYPE_PM25_AVG_24H_CH2: ("PM2.5 24h average 2", - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - TYPE_SENSOR, None, "mdi:eye", 0), - TYPE_PM25_AVG_24H_CH3: ("PM2.5 24h average 3", - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - TYPE_SENSOR, None, "mdi:eye", 0), - TYPE_PM25_AVG_24H_CH4: ("PM2.5 24h average 4", - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - TYPE_SENSOR, None, "mdi:eye", 0), - TYPE_LIGHTNING_TIME: ("Last Lightning strike", "", - TYPE_SENSOR, DEVICE_CLASS_TIMESTAMP, "mdi:clock", 0), - TYPE_LIGHTNING_NUM: ("Lightning strikes", f"strikes/{TIME_DAYS}", - TYPE_SENSOR, None, "mdi:weather-lightning", 0), - TYPE_LIGHTNING_KM: ("Lightning strike distance", LENGTH_KILOMETERS, - TYPE_SENSOR, None, "mdi:ruler", S_METRIC), - TYPE_LIGHTNING_MI: ("Lightning strike distance", LENGTH_MILES, - TYPE_SENSOR, None, "mdi:ruler", S_IMPERIAL), - TYPE_LEAK_CH1: ("Leak Detection 1", LEAK_DETECTED, TYPE_BINARY_SENSOR, - DEVICE_CLASS_MOISTURE, "mdi:leak", 0), - TYPE_LEAK_CH2: ("Leak Detection 2", LEAK_DETECTED, TYPE_BINARY_SENSOR, - DEVICE_CLASS_MOISTURE, "mdi:leak", 0), - TYPE_LEAK_CH3: ("Leak Detection 3", LEAK_DETECTED, TYPE_BINARY_SENSOR, - DEVICE_CLASS_MOISTURE, "mdi:leak", 0), - TYPE_LEAK_CH4: ("Leak Detection 4", LEAK_DETECTED, TYPE_BINARY_SENSOR, - DEVICE_CLASS_MOISTURE, "mdi:leak", 0), - TYPE_WH25BATT: ("WH25 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_WH26BATT: ("WH26 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_WH40BATT: ("WH40 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_WH57BATT: ("WH57 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_WH65BATT: ("WH65 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_WH68BATT: ("WH68 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_WH80BATT: ("WH80 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_SOILBATT1: ("Soil Moisture 1 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_SOILBATT2: ("Soil Moisture 2 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_SOILBATT3: ("Soil Moisture 3 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_SOILBATT4: ("Soil Moisture 4 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_SOILBATT5: ("Soil Moisture 5 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_SOILBATT6: ("Soil Moisture 6 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_SOILBATT7: ("Soil Moisture 7 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_SOILBATT8: ("Soil Moisture 8 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_BATTERY1: ("Battery 1", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_BATTERY2: ("Battery 2", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_BATTERY3: ("Battery 3", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_BATTERY4: ("Battery 4", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_BATTERY5: ("Battery 5", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_BATTERY6: ("Battery 6", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_BATTERY7: ("Battery 7", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_BATTERY8: ("Battery 8", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_PM25BATT1: ("PM2.5 1 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_PM25BATT2: ("PM2.5 2 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_PM25BATT3: ("PM2.5 3 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_PM25BATT4: ("PM2.5 4 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_PM25BATT5: ("PM2.5 5 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_PM25BATT6: ("PM2.5 6 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_PM25BATT7: ("PM2.5 7 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_PM25BATT8: ("PM2.5 8 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_LEAKBATT1: ("Leak 1 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_LEAKBATT2: ("Leak 2 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_LEAKBATT3: ("Leak 3 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_LEAKBATT4: ("Leak 4 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_LEAKBATT5: ("Leak 5 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_LEAKBATT6: ("Leak 6 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_LEAKBATT7: ("Leak 7 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_LEAKBATT8: ("Leak 8 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), -} - -IGNORED_SENSORS = [ - 'tempinf', - 'tempf', - 'temp1f', - 'temp2f', - 'temp3f', - 'temp4f', - 'temp5f', - 'temp6f', - 'temp7f', - 'temp8f', - 'dateutc', - 'windgustms', - 'windspeedms', - 'windspdms_avg10m', - 'maxdailygustms', - 'windchillf', - 'dewpointf', - 'dewpointinf', - 'dewpoint1f', - 'dewpoint2f', - 'dewpoint3f', - 'dewpoint4f', - 'dewpoint5f', - 'dewpoint6f', - 'dewpoint7f', - 'dewpoint8f', +from .const import ( + CONF_UNIT_BARO, + CONF_UNIT_WIND, + CONF_UNIT_RAIN, + CONF_UNIT_WINDCHILL, + CONF_UNIT_LIGHTNING, + DOMAIN, + DATA_CONFIG, + DATA_ECOWITT, + DATA_STATION, DATA_PASSKEY, DATA_STATIONTYPE, DATA_FREQ, DATA_MODEL, -] + DATA_READY, + IGNORED_SENSORS, + S_IMPERIAL, + S_METRIC, + SENSOR_TYPES, + TYPE_SENSOR, + TYPE_BINARY_SENSOR, + W_TYPE_NEW, + W_TYPE_OLD, + W_TYPE_HYBRID, +) + +_LOGGER = logging.getLogger(__name__) COMPONENT_SCHEMA = vol.Schema( { diff --git a/custom_components/ecowitt/const.py b/custom_components/ecowitt/const.py new file mode 100644 index 0000000..0b33282 --- /dev/null +++ b/custom_components/ecowitt/const.py @@ -0,0 +1,515 @@ +"""Constants used by ecowitt component.""" + +from homeassistant.const import ( + DEGREE, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + POWER_WATT, + TEMP_CELSIUS, + PERCENTAGE, + PRESSURE_HPA, + PRESSURE_INHG, + LENGTH_INCHES, + LENGTH_KILOMETERS, + LENGTH_MILES, + SPEED_KILOMETERS_PER_HOUR, + SPEED_MILES_PER_HOUR, + TIME_HOURS, + TIME_DAYS, + TIME_WEEKS, + TIME_MONTHS, + TIME_YEARS, + UV_INDEX, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TIMESTAMP, +) + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOISTURE, +) + +TYPE_SENSOR = "sensor" +TYPE_BINARY_SENSOR = "binary_sensor" +DOMAIN = "ecowitt" +DATA_CONFIG = "config" +DATA_ECOWITT = "ecowitt_listener" +DATA_STATION = "station" +DATA_PASSKEY = "PASSKEY" +DATA_STATIONTYPE = "stationtype" +DATA_FREQ = "freq" +DATA_MODEL = "model" +DATA_READY = "ready" + +CONF_UNIT_BARO = "barounit" +CONF_UNIT_WIND = "windunit" +CONF_UNIT_RAIN = "rainunit" +CONF_UNIT_WINDCHILL = "windchillunit" +CONF_UNIT_LIGHTNING = "lightningunit" + +TYPE_BAROMABSHPA = "baromabshpa" +TYPE_BAROMRELHPA = "baromrelhpa" +TYPE_BAROMABSIN = "baromabsin" +TYPE_BAROMRELIN = "baromrelin" +TYPE_RAINRATEIN = "rainratein" +TYPE_EVENTRAININ = "eventrainin" +TYPE_HOURLYRAININ = "hourlyrainin" +TYPE_TOTALRAININ = "totalrainin" +TYPE_DAILYRAININ = "dailyrainin" +TYPE_WEEKLYRAININ = "weeklyrainin" +TYPE_MONTHLYRAININ = "monthlyrainin" +TYPE_YEARLYRAININ = "yearlyrainin" +TYPE_RAINRATEMM = "rainratemm" +TYPE_EVENTRAINMM = "eventrainmm" +TYPE_HOURLYRAINMM = "hourlyrainmm" +TYPE_TOTALRAINMM = "totalrainmm" +TYPE_DAILYRAINMM = "dailyrainmm" +TYPE_WEEKLYRAINMM = "weeklyrainmm" +TYPE_MONTHLYRAINMM = "monthlyrainmm" +TYPE_YEARLYRAINMM = "yearlyrainmm" +TYPE_HUMIDITY = "humidity" +TYPE_HUMIDITY1 = "humidity1" +TYPE_HUMIDITY2 = "humidity2" +TYPE_HUMIDITY3 = "humidity3" +TYPE_HUMIDITY4 = "humidity4" +TYPE_HUMIDITY5 = "humidity5" +TYPE_HUMIDITY6 = "humidity6" +TYPE_HUMIDITY7 = "humidity7" +TYPE_HUMIDITY8 = "humidity8" +TYPE_HUMIDITYIN = "humidityin" +TYPE_WINDDIR = "winddir" +TYPE_WINDDIR_A10 = "winddir_avg10m" +TYPE_WINDSPEEDKMH = "windspeedkmh" +TYPE_WINDSPEEDKMH_A10 = "windspdkmh_avg10m" +TYPE_WINDGUSTKMH = "windgustkmh" +TYPE_WINDSPEEDMPH = "windspeedmph" +TYPE_WINDSPEEDMPH_A10 = "windspdmph_avg10m" +TYPE_WINDGUSTMPH = "windgustmph" +TYPE_MAXDAILYGUST = "maxdailygust" +TYPE_MAXDAILYGUSTKMH = "maxdailygustkmh" +TYPE_TEMPC = "tempc" +TYPE_TEMPINC = "tempinc" +TYPE_TEMP1C = "temp1c" +TYPE_TEMP2C = "temp2c" +TYPE_TEMP3C = "temp3c" +TYPE_TEMP4C = "temp4c" +TYPE_TEMP5C = "temp5c" +TYPE_TEMP6C = "temp6c" +TYPE_TEMP7C = "temp7c" +TYPE_TEMP8C = "temp8c" +TYPE_DEWPOINTC = "dewpointc" +TYPE_DEWPOINTINC = "dewpointinc" +TYPE_DEWPOINT1C = "dewpoint1c" +TYPE_DEWPOINT2C = "dewpoint2c" +TYPE_DEWPOINT3C = "dewpoint3c" +TYPE_DEWPOINT4C = "dewpoint4c" +TYPE_DEWPOINT5C = "dewpoint5c" +TYPE_DEWPOINT6C = "dewpoint6c" +TYPE_DEWPOINT7C = "dewpoint7c" +TYPE_DEWPOINT8C = "dewpoint8c" +TYPE_WINDCHILLC = "windchillc" +TYPE_SOLARRADIATION = "solarradiation" +TYPE_UV = "uv" +TYPE_SOILMOISTURE1 = "soilmoisture1" +TYPE_SOILMOISTURE2 = "soilmoisture2" +TYPE_SOILMOISTURE3 = "soilmoisture3" +TYPE_SOILMOISTURE4 = "soilmoisture4" +TYPE_SOILMOISTURE5 = "soilmoisture5" +TYPE_SOILMOISTURE6 = "soilmoisture6" +TYPE_SOILMOISTURE7 = "soilmoisture7" +TYPE_SOILMOISTURE8 = "soilmoisture8" +TYPE_PM25_CH1 = "pm25_ch1" +TYPE_PM25_CH2 = "pm25_ch2" +TYPE_PM25_CH3 = "pm25_ch3" +TYPE_PM25_CH4 = "pm25_ch4" +TYPE_PM25_AVG_24H_CH1 = "pm25_avg_24h_ch1" +TYPE_PM25_AVG_24H_CH2 = "pm25_avg_24h_ch2" +TYPE_PM25_AVG_24H_CH3 = "pm25_avg_24h_ch3" +TYPE_PM25_AVG_24H_CH4 = "pm25_avg_24h_ch4" +TYPE_LIGHTNING_TIME = "lightning_time" +TYPE_LIGHTNING_NUM = "lightning_num" +TYPE_LIGHTNING_KM = "lightning" +TYPE_LIGHTNING_MI = "lightning_mi" +TYPE_LEAK_CH1 = "leak_ch1" +TYPE_LEAK_CH2 = "leak_ch2" +TYPE_LEAK_CH3 = "leak_ch3" +TYPE_LEAK_CH4 = "leak_ch4" +TYPE_WH25BATT = "wh25batt" +TYPE_WH26BATT = "wh26batt" +TYPE_WH40BATT = "wh40batt" +TYPE_WH57BATT = "wh57batt" +TYPE_WH68BATT = "wh68batt" +TYPE_WH65BATT = "wh65batt" +TYPE_WH80BATT = "wh80batt" +TYPE_SOILBATT1 = "soilbatt1" +TYPE_SOILBATT2 = "soilbatt2" +TYPE_SOILBATT3 = "soilbatt3" +TYPE_SOILBATT4 = "soilbatt4" +TYPE_SOILBATT5 = "soilbatt5" +TYPE_SOILBATT6 = "soilbatt6" +TYPE_SOILBATT7 = "soilbatt7" +TYPE_SOILBATT8 = "soilbatt8" +TYPE_BATTERY1 = "batt1" +TYPE_BATTERY2 = "batt2" +TYPE_BATTERY3 = "batt3" +TYPE_BATTERY4 = "batt4" +TYPE_BATTERY5 = "batt5" +TYPE_BATTERY6 = "batt6" +TYPE_BATTERY7 = "batt7" +TYPE_BATTERY8 = "batt8" +TYPE_PM25BATT1 = "pm25batt1" +TYPE_PM25BATT2 = "pm25batt2" +TYPE_PM25BATT3 = "pm25batt3" +TYPE_PM25BATT4 = "pm25batt4" +TYPE_PM25BATT5 = "pm25batt5" +TYPE_PM25BATT6 = "pm25batt6" +TYPE_PM25BATT7 = "pm25batt7" +TYPE_PM25BATT8 = "pm25batt8" +TYPE_LEAKBATT1 = "leakbatt1" +TYPE_LEAKBATT2 = "leakbatt2" +TYPE_LEAKBATT3 = "leakbatt3" +TYPE_LEAKBATT4 = "leakbatt4" +TYPE_LEAKBATT5 = "leakbatt5" +TYPE_LEAKBATT6 = "leakbatt6" +TYPE_LEAKBATT7 = "leakbatt7" +TYPE_LEAKBATT8 = "leakbatt8" + +S_METRIC = 1 +S_IMPERIAL = 2 + +W_TYPE_NEW = "new" +W_TYPE_OLD = "old" +W_TYPE_HYBRID = "hybrid" + +LEAK_DETECTED = "Leak Detected" + +# Name, unit_of_measure, type, device_class, icon, metric=1 +# name, uom, kind, device_class, icon, metric = SENSOR_TYPES[x] +SENSOR_TYPES = { + TYPE_BAROMABSHPA: ("Absolute Pressure", PRESSURE_HPA, + TYPE_SENSOR, DEVICE_CLASS_PRESSURE, + "mdi:gauge", S_METRIC), + TYPE_BAROMRELHPA: ("Relative Pressure", PRESSURE_HPA, + TYPE_SENSOR, DEVICE_CLASS_PRESSURE, + "mdi:gauge", S_METRIC), + TYPE_BAROMABSIN: ("Absolute Pressure", PRESSURE_INHG, + TYPE_SENSOR, DEVICE_CLASS_PRESSURE, + "mdi:gauge", S_IMPERIAL), + TYPE_BAROMRELIN: ("Relative Pressure", PRESSURE_INHG, + TYPE_SENSOR, DEVICE_CLASS_PRESSURE, + "mdi:gauge", S_IMPERIAL), + TYPE_RAINRATEIN: ("Rain Rate", f"{LENGTH_INCHES}/{TIME_HOURS}", + TYPE_SENSOR, None, "mdi:water", S_IMPERIAL), + TYPE_EVENTRAININ: ("Event Rain Rate", f"{LENGTH_INCHES}/{TIME_HOURS}", + TYPE_SENSOR, None, "mdi:water", S_IMPERIAL), + TYPE_HOURLYRAININ: ("Hourly Rain Rate", f"{LENGTH_INCHES}/{TIME_HOURS}", + TYPE_SENSOR, None, "mdi:water", S_IMPERIAL), + TYPE_TOTALRAININ: ("Total Rain Rate", f"{LENGTH_INCHES}", + TYPE_SENSOR, None, "mdi:water", S_IMPERIAL), + TYPE_DAILYRAININ: ("Daily Rain Rate", f"{LENGTH_INCHES}/{TIME_DAYS}", + TYPE_SENSOR, None, "mdi:water", S_IMPERIAL), + TYPE_WEEKLYRAININ: ("Weekly Rain Rate", f"{LENGTH_INCHES}/{TIME_WEEKS}", + TYPE_SENSOR, None, "mdi:water", S_IMPERIAL), + TYPE_MONTHLYRAININ: ("Monthly Rain Rate", f"{LENGTH_INCHES}/{TIME_MONTHS}", + TYPE_SENSOR, None, "mdi:water", S_IMPERIAL), + TYPE_YEARLYRAININ: ("Yearly Rain Rate", f"{LENGTH_INCHES}/{TIME_YEARS}", + TYPE_SENSOR, None, "mdi:water", S_IMPERIAL), + TYPE_RAINRATEMM: ("Rain Rate", f"mm/{TIME_HOURS}", + TYPE_SENSOR, None, "mdi:water", S_METRIC), + TYPE_EVENTRAINMM: ("Event Rain Rate", f"mm/{TIME_HOURS}", + TYPE_SENSOR, None, "mdi:water", S_METRIC), + TYPE_HOURLYRAINMM: ("Hourly Rain Rate", f"mm/{TIME_HOURS}", + TYPE_SENSOR, None, "mdi:water", S_METRIC), + TYPE_TOTALRAINMM: ("Total Rain Rate", f"mm", + TYPE_SENSOR, None, "mdi:water", S_METRIC), + TYPE_DAILYRAINMM: ("Daily Rain Rate", f"mm/{TIME_DAYS}", + TYPE_SENSOR, None, "mdi:water", S_METRIC), + TYPE_WEEKLYRAINMM: ("Weekly Rain Rate", f"mm/{TIME_WEEKS}", + TYPE_SENSOR, None, "mdi:water", S_METRIC), + TYPE_MONTHLYRAINMM: ("Monthly Rain Rate", f"mm/{TIME_MONTHS}", + TYPE_SENSOR, None, "mdi:water", S_METRIC), + TYPE_YEARLYRAINMM: ("Yearly Rain Rate", f"mm/{TIME_YEARS}", + TYPE_SENSOR, None, "mdi:water", S_METRIC), + TYPE_HUMIDITY: ("Humidity", PERCENTAGE, + TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, + "mdi:water-percent", 0), + TYPE_HUMIDITYIN: ("Indoor Humidity", PERCENTAGE, + TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, + "mdi:water-percent", 0), + TYPE_HUMIDITY1: ("Humidity 1", PERCENTAGE, + TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, + "mdi:water-percent", 0), + TYPE_HUMIDITY2: ("Humidity 2", PERCENTAGE, + TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, + "mdi:water-percent", 0), + TYPE_HUMIDITY3: ("Humidity 3", PERCENTAGE, + TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, + "mdi:water-percent", 0), + TYPE_HUMIDITY4: ("Humidity 4", PERCENTAGE, + TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, + "mdi:water-percent", 0), + TYPE_HUMIDITY5: ("Humidity 5", PERCENTAGE, + TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, + "mdi:water-percent", 0), + TYPE_HUMIDITY6: ("Humidity 6", PERCENTAGE, + TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, + "mdi:water-percent", 0), + TYPE_HUMIDITY7: ("Humidity 7", PERCENTAGE, + TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, + "mdi:water-percent", 0), + TYPE_HUMIDITY8: ("Humidity 8", PERCENTAGE, + TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, + "mdi:water-percent", 0), + TYPE_WINDDIR: ("Wind Direction", DEGREE, + TYPE_SENSOR, None, "mdi:water-percent", 0), + TYPE_WINDDIR_A10: ("Wind Direction 10m Avg", DEGREE, + TYPE_SENSOR, None, "mdi:water-percent", 0), + TYPE_WINDSPEEDKMH: ("Wind Speed", SPEED_KILOMETERS_PER_HOUR, + TYPE_SENSOR, None, "mdi:weather-windy", S_METRIC), + TYPE_WINDSPEEDKMH_A10: ("Wind Speed 10m Avg", SPEED_KILOMETERS_PER_HOUR, + TYPE_SENSOR, None, "mdi:weather-windy", S_METRIC), + TYPE_WINDGUSTKMH: ("Wind Gust", SPEED_KILOMETERS_PER_HOUR, + TYPE_SENSOR, None, "mdi:weather-windy", S_METRIC), + TYPE_WINDSPEEDMPH: ("Wind Speed", SPEED_MILES_PER_HOUR, + TYPE_SENSOR, None, "mdi:weather-windy", S_IMPERIAL), + TYPE_WINDSPEEDMPH_A10: ("Wind Speed 10m Avg", SPEED_MILES_PER_HOUR, + TYPE_SENSOR, None, "mdi:weather-windy", + S_IMPERIAL), + TYPE_WINDGUSTMPH: ("Wind Gust", SPEED_MILES_PER_HOUR, + TYPE_SENSOR, None, "mdi:weather-windy", S_IMPERIAL), + TYPE_MAXDAILYGUST: ("Max Daily Wind Gust", SPEED_MILES_PER_HOUR, + TYPE_SENSOR, None, "mdi:weather-windy", S_IMPERIAL), + TYPE_MAXDAILYGUSTKMH: ("Max Daily Wind Gust", SPEED_KILOMETERS_PER_HOUR, + TYPE_SENSOR, None, "mdi:weather-windy", S_METRIC), + TYPE_TEMPC: ("Outdoor Temperature", TEMP_CELSIUS, + TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, "mdi:thermometer", 0), + TYPE_TEMP1C: ("Temperature 1", TEMP_CELSIUS, + TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, "mdi:thermometer", 0), + TYPE_TEMP2C: ("Temperature 2", TEMP_CELSIUS, + TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, "mdi:thermometer", 0), + TYPE_TEMP3C: ("Temperature 3", TEMP_CELSIUS, + TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, "mdi:thermometer", 0), + TYPE_TEMP4C: ("Temperature 4", TEMP_CELSIUS, + TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, "mdi:thermometer", 0), + TYPE_TEMP5C: ("Temperature 5", TEMP_CELSIUS, + TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, "mdi:thermometer", 0), + TYPE_TEMP6C: ("Temperature 6", TEMP_CELSIUS, + TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, "mdi:thermometer", 0), + TYPE_TEMP7C: ("Temperature 7", TEMP_CELSIUS, + TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, "mdi:thermometer", 0), + TYPE_TEMP8C: ("Temperature 8", TEMP_CELSIUS, + TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, "mdi:thermometer", 0), + TYPE_TEMPINC: ("Indoor Temperature", TEMP_CELSIUS, + TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, + "mdi:thermometer", 0), + TYPE_DEWPOINTC: ("Dewpoint", TEMP_CELSIUS, + TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, + "mdi:thermometer", 0), + TYPE_DEWPOINTINC: ("Indoor Dewpoint", TEMP_CELSIUS, + TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, + "mdi:thermometer", 0), + TYPE_DEWPOINT1C: ("Dewpoint 1", TEMP_CELSIUS, + TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, + "mdi:thermometer", 0), + TYPE_DEWPOINT2C: ("Dewpoint 2", TEMP_CELSIUS, + TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, + "mdi:thermometer", 0), + TYPE_DEWPOINT3C: ("Dewpoint 3", TEMP_CELSIUS, + TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, + "mdi:thermometer", 0), + TYPE_DEWPOINT4C: ("Dewpoint 4", TEMP_CELSIUS, + TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, + "mdi:thermometer", 0), + TYPE_DEWPOINT5C: ("Dewpoint 5", TEMP_CELSIUS, + TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, + "mdi:thermometer", 0), + TYPE_DEWPOINT6C: ("Dewpoint 6", TEMP_CELSIUS, + TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, + "mdi:thermometer", 0), + TYPE_DEWPOINT7C: ("Dewpoint 7", TEMP_CELSIUS, + TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, + "mdi:thermometer", 0), + TYPE_DEWPOINT8C: ("Dewpoint 8", TEMP_CELSIUS, + TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, + "mdi:thermometer", 0), + TYPE_WINDCHILLC: ("Windchill", TEMP_CELSIUS, + TYPE_SENSOR, DEVICE_CLASS_TEMPERATURE, + "mdi:thermometer", 0), + TYPE_SOLARRADIATION: ("Solar Radiation", f"{POWER_WATT}/m^2", + TYPE_SENSOR, DEVICE_CLASS_ILLUMINANCE, + "mdi:weather-sunny", 0), + TYPE_UV: ("UV Index", UV_INDEX, + TYPE_SENSOR, None, "mdi:sunglasses", 0), + TYPE_SOILMOISTURE1: ("Soil Moisture 1", PERCENTAGE, + TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, + "mdi:water-percent", 0), + TYPE_SOILMOISTURE2: ("Soil Moisture 2", PERCENTAGE, + TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, + "mdi:water-percent", 0), + TYPE_SOILMOISTURE3: ("Soil Moisture 3", PERCENTAGE, + TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, + "mdi:water-percent", 0), + TYPE_SOILMOISTURE4: ("Soil Moisture 4", PERCENTAGE, + TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, + "mdi:water-percent", 0), + TYPE_SOILMOISTURE5: ("Soil Moisture 5", PERCENTAGE, + TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, + "mdi:water-percent", 0), + TYPE_SOILMOISTURE6: ("Soil Moisture 6", PERCENTAGE, + TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, + "mdi:water-percent", 0), + TYPE_SOILMOISTURE7: ("Soil Moisture 7", PERCENTAGE, + TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, + "mdi:water-percent", 0), + TYPE_SOILMOISTURE8: ("Soil Moisture 8", PERCENTAGE, + TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, + "mdi:water-percent", 0), + TYPE_PM25_CH1: ("PM2.5 1", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + TYPE_SENSOR, None, "mdi:eye", 0), + TYPE_PM25_CH2: ("PM2.5 2", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + TYPE_SENSOR, None, "mdi:eye", 0), + TYPE_PM25_CH3: ("PM2.5 3", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + TYPE_SENSOR, None, "mdi:eye", 0), + TYPE_PM25_CH4: ("PM2.5 4", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + TYPE_SENSOR, None, "mdi:eye", 0), + TYPE_PM25_AVG_24H_CH1: ("PM2.5 24h average 1", + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + TYPE_SENSOR, None, "mdi:eye", 0), + TYPE_PM25_AVG_24H_CH2: ("PM2.5 24h average 2", + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + TYPE_SENSOR, None, "mdi:eye", 0), + TYPE_PM25_AVG_24H_CH3: ("PM2.5 24h average 3", + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + TYPE_SENSOR, None, "mdi:eye", 0), + TYPE_PM25_AVG_24H_CH4: ("PM2.5 24h average 4", + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + TYPE_SENSOR, None, "mdi:eye", 0), + TYPE_LIGHTNING_TIME: ("Last Lightning strike", "", + TYPE_SENSOR, DEVICE_CLASS_TIMESTAMP, "mdi:clock", 0), + TYPE_LIGHTNING_NUM: ("Lightning strikes", f"strikes/{TIME_DAYS}", + TYPE_SENSOR, None, "mdi:weather-lightning", 0), + TYPE_LIGHTNING_KM: ("Lightning strike distance", LENGTH_KILOMETERS, + TYPE_SENSOR, None, "mdi:ruler", S_METRIC), + TYPE_LIGHTNING_MI: ("Lightning strike distance", LENGTH_MILES, + TYPE_SENSOR, None, "mdi:ruler", S_IMPERIAL), + TYPE_LEAK_CH1: ("Leak Detection 1", LEAK_DETECTED, TYPE_BINARY_SENSOR, + DEVICE_CLASS_MOISTURE, "mdi:leak", 0), + TYPE_LEAK_CH2: ("Leak Detection 2", LEAK_DETECTED, TYPE_BINARY_SENSOR, + DEVICE_CLASS_MOISTURE, "mdi:leak", 0), + TYPE_LEAK_CH3: ("Leak Detection 3", LEAK_DETECTED, TYPE_BINARY_SENSOR, + DEVICE_CLASS_MOISTURE, "mdi:leak", 0), + TYPE_LEAK_CH4: ("Leak Detection 4", LEAK_DETECTED, TYPE_BINARY_SENSOR, + DEVICE_CLASS_MOISTURE, "mdi:leak", 0), + TYPE_WH25BATT: ("WH25 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_WH26BATT: ("WH26 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_WH40BATT: ("WH40 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_WH57BATT: ("WH57 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_WH65BATT: ("WH65 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_WH68BATT: ("WH68 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_WH80BATT: ("WH80 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_SOILBATT1: ("Soil Moisture 1 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_SOILBATT2: ("Soil Moisture 2 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_SOILBATT3: ("Soil Moisture 3 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_SOILBATT4: ("Soil Moisture 4 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_SOILBATT5: ("Soil Moisture 5 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_SOILBATT6: ("Soil Moisture 6 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_SOILBATT7: ("Soil Moisture 7 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_SOILBATT8: ("Soil Moisture 8 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_BATTERY1: ("Battery 1", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_BATTERY2: ("Battery 2", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_BATTERY3: ("Battery 3", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_BATTERY4: ("Battery 4", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_BATTERY5: ("Battery 5", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_BATTERY6: ("Battery 6", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_BATTERY7: ("Battery 7", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_BATTERY8: ("Battery 8", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_PM25BATT1: ("PM2.5 1 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_PM25BATT2: ("PM2.5 2 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_PM25BATT3: ("PM2.5 3 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_PM25BATT4: ("PM2.5 4 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_PM25BATT5: ("PM2.5 5 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_PM25BATT6: ("PM2.5 6 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_PM25BATT7: ("PM2.5 7 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_PM25BATT8: ("PM2.5 8 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_LEAKBATT1: ("Leak 1 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_LEAKBATT2: ("Leak 2 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_LEAKBATT3: ("Leak 3 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_LEAKBATT4: ("Leak 4 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_LEAKBATT5: ("Leak 5 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_LEAKBATT6: ("Leak 6 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_LEAKBATT7: ("Leak 7 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), + TYPE_LEAKBATT8: ("Leak 8 Battery", "BATT", TYPE_SENSOR, + None, "mdi:battery", 0), +} + +IGNORED_SENSORS = [ + 'tempinf', + 'tempf', + 'temp1f', + 'temp2f', + 'temp3f', + 'temp4f', + 'temp5f', + 'temp6f', + 'temp7f', + 'temp8f', + 'dateutc', + 'windgustms', + 'windspeedms', + 'windspdms_avg10m', + 'maxdailygustms', + 'windchillf', + 'dewpointf', + 'dewpointinf', + 'dewpoint1f', + 'dewpoint2f', + 'dewpoint3f', + 'dewpoint4f', + 'dewpoint5f', + 'dewpoint6f', + 'dewpoint7f', + 'dewpoint8f', + DATA_PASSKEY, + DATA_STATIONTYPE, + DATA_FREQ, + DATA_MODEL, +] From 0296791f406fe57aceb25cd3da26b04a02205eb0 Mon Sep 17 00:00:00 2001 From: garbled1 Date: Fri, 13 Nov 2020 11:53:23 -0700 Subject: [PATCH 02/28] I seriously doubt this works.. trying anyhow.. --- custom_components/ecowitt/__init__.py | 71 ++++++------ custom_components/ecowitt/config_flow.py | 104 ++++++++++++++++++ custom_components/ecowitt/const.py | 5 + .../ecowitt/translations/en.json | 26 +++++ 4 files changed, 175 insertions(+), 31 deletions(-) create mode 100644 custom_components/ecowitt/config_flow.py create mode 100644 custom_components/ecowitt/translations/en.json diff --git a/custom_components/ecowitt/__init__.py b/custom_components/ecowitt/__init__.py index 7e689ab..0a3a543 100644 --- a/custom_components/ecowitt/__init__.py +++ b/custom_components/ecowitt/__init__.py @@ -11,6 +11,7 @@ ) import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.core import HomeAssistant, callback @@ -41,6 +42,7 @@ DATA_FREQ, DATA_MODEL, DATA_READY, + ECOWITT_PLATFORMS, IGNORED_SENSORS, S_IMPERIAL, S_METRIC, @@ -73,38 +75,45 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: COMPONENT_SCHEMA}, extra=vol.ALLOW_EXTRA) -async def async_setup(hass: HomeAssistant, config): - """Set up the Ecowitt component.""" - +async def async_setup(hass: HomeAssistant, config: dict): + """Configure the Ecowitt component using flow only.""" hass.data[DOMAIN] = {} - sensor_sensors = [] - binary_sensors = [] - if DOMAIN not in config: - return True + if DOMAIN in config: + for entry in config[DOMAIN]: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=entry + ) + ) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up the Ecowitt component from a config entry.""" - conf = config[DOMAIN] + sensor_sensors = [] + binary_sensors = [] # Store config - hass.data[DOMAIN][DATA_CONFIG] = conf - hass.data[DOMAIN][DATA_STATION] = {} - hass.data[DOMAIN][DATA_READY] = False + hass.data[DOMAIN][entry.entry_id][DATA_STATION] = {} + hass.data[DOMAIN][entry.entry_id][DATA_READY] = False # preload some model info - stationinfo = hass.data[DOMAIN][DATA_STATION] + stationinfo = hass.data[DOMAIN][entry.entry_id][DATA_STATION] stationinfo[DATA_STATIONTYPE] = "Unknown" stationinfo[DATA_FREQ] = "Unknown" stationinfo[DATA_MODEL] = "Unknown" # setup the base connection - ws = EcoWittListener(port=conf[CONF_PORT]) - hass.data[DOMAIN][DATA_ECOWITT] = ws + ws = EcoWittListener(port=entry.data[CONF_PORT]) + hass.data[DOMAIN][entry.entry_id][DATA_ECOWITT] = ws - if conf[CONF_UNIT_WINDCHILL] == W_TYPE_OLD: + if entry.data[CONF_UNIT_WINDCHILL] == W_TYPE_OLD: ws.set_windchill(WINDCHILL_OLD) - if conf[CONF_UNIT_WINDCHILL] == W_TYPE_NEW: + if entry.data[CONF_UNIT_WINDCHILL] == W_TYPE_NEW: ws.set_windchill(WINDCHILL_NEW) - if conf[CONF_UNIT_WINDCHILL] == W_TYPE_HYBRID: + if entry.data[CONF_UNIT_WINDCHILL] == W_TYPE_HYBRID: ws.set_windchill(WINDCHILL_HYBRID) hass.loop.create_task(ws.listen()) @@ -123,31 +132,31 @@ def check_imp_metric_sensor(sensor): # Is this a metric or imperial sensor, lookup and skip name, uom, kind, device_class, icon, metric = SENSOR_TYPES[sensor] if "baro" in sensor: - if (conf[CONF_UNIT_BARO] == CONF_UNIT_SYSTEM_IMPERIAL + if (entry.data[CONF_UNIT_BARO] == CONF_UNIT_SYSTEM_IMPERIAL and metric == S_METRIC): return False - if (conf[CONF_UNIT_BARO] == CONF_UNIT_SYSTEM_METRIC + if (entry.data[CONF_UNIT_BARO] == CONF_UNIT_SYSTEM_METRIC and metric == S_IMPERIAL): return False if "rain" in sensor: - if (conf[CONF_UNIT_RAIN] == CONF_UNIT_SYSTEM_IMPERIAL + if (entry.data[CONF_UNIT_RAIN] == CONF_UNIT_SYSTEM_IMPERIAL and metric == S_METRIC): return False - if (conf[CONF_UNIT_RAIN] == CONF_UNIT_SYSTEM_METRIC + if (entry.data[CONF_UNIT_RAIN] == CONF_UNIT_SYSTEM_METRIC and metric == S_IMPERIAL): return False if "wind" in sensor: - if (conf[CONF_UNIT_WIND] == CONF_UNIT_SYSTEM_IMPERIAL + if (entry.data[CONF_UNIT_WIND] == CONF_UNIT_SYSTEM_IMPERIAL and metric == S_METRIC): return False - if (conf[CONF_UNIT_WIND] == CONF_UNIT_SYSTEM_METRIC + if (entry.data[CONF_UNIT_WIND] == CONF_UNIT_SYSTEM_METRIC and metric == S_IMPERIAL): return False if (sensor == 'lightning' - and conf[CONF_UNIT_LIGHTNING] == CONF_UNIT_SYSTEM_IMPERIAL): + and entry.data[CONF_UNIT_LIGHTNING] == CONF_UNIT_SYSTEM_IMPERIAL): return False if (sensor == 'lightning_mi' - and conf[CONF_UNIT_LIGHTNING] == CONF_UNIT_SYSTEM_METRIC): + and entry.data[CONF_UNIT_LIGHTNING] == CONF_UNIT_SYSTEM_METRIC): return False return True @@ -199,19 +208,19 @@ async def _first_data_rec(weather_data): if sensor_sensors: hass.async_create_task( async_load_platform(hass, "sensor", DOMAIN, sensor_sensors, - config) + entry.data) ) if binary_sensors: hass.async_create_task( async_load_platform(hass, "binary_sensor", DOMAIN, binary_sensors, - config) + entry.data) ) - hass.data[DOMAIN][DATA_READY] = True + hass.data[DOMAIN][entry.entry_id][DATA_READY] = True async def _async_ecowitt_update_cb(weather_data): """Primary update callback called from pyecowitt.""" _LOGGER.debug("Primary update callback triggered.") - if not hass.data[DOMAIN][DATA_READY]: + if not hass.data[DOMAIN][entry.entry_id][DATA_READY]: await _first_data_rec(weather_data) return for sensor in weather_data.keys(): @@ -232,12 +241,12 @@ async def _async_ecowitt_update_cb(weather_data): if kind == TYPE_SENSOR: hass.async_create_task( async_load_platform(hass, "sensor", DOMAIN, - new_sensor, config) + new_sensor, entry.data) ) if kind == TYPE_BINARY_SENSOR: hass.async_create_task( async_load_platform(hass, "binary_sensor", DOMAIN, - new_sensor, config) + new_sensor, entry.data) ) async_dispatcher_send(hass, DOMAIN) diff --git a/custom_components/ecowitt/config_flow.py b/custom_components/ecowitt/config_flow.py new file mode 100644 index 0000000..2ec2809 --- /dev/null +++ b/custom_components/ecowitt/config_flow.py @@ -0,0 +1,104 @@ +"""Config flow for ecowitt.""" +import logging + +from pyecowitt import ( + EcoWittListener, + WINDCHILL_OLD, + WINDCHILL_NEW, + WINDCHILL_HYBRID, +) + +import voluptuous as vol +import homeassistant.helpers.config_validation as cv +from homeassistant import config_entries, core, exceptions + +from homeassistant.const import ( + CONF_PORT, + CONF_UNIT_SYSTEM_METRIC, + CONF_UNIT_SYSTEM_IMPERIAL, +) + +from .const import ( + CONF_NAME, + CONF_UNIT_BARO, + CONF_UNIT_WIND, + CONF_UNIT_RAIN, + CONF_UNIT_WINDCHILL, + CONF_UNIT_LIGHTNING, + DEFAULT_PORT, + DOMAIN, + DATA_CONFIG, + DATA_ECOWITT, + DATA_STATION, + DATA_PASSKEY, + DATA_STATIONTYPE, + DATA_FREQ, + DATA_MODEL, + DATA_READY, + W_TYPE_NEW, + W_TYPE_OLD, + W_TYPE_HYBRID, +) + +UNIT_OPTS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL] +WINDCHILL_OPTS = [W_TYPE_HYBRID, W_TYPE_NEW, W_TYPE_OLD] + + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_UNIT_BARO, + default=CONF_UNIT_SYSTEM_METRIC): vol.In(UNIT_OPTS), + vol.Optional(CONF_UNIT_WIND, + default=CONF_UNIT_SYSTEM_IMPERIAL): vol.In(UNIT_OPTS), + vol.Optional(CONF_UNIT_RAIN, + default=CONF_UNIT_SYSTEM_IMPERIAL): vol.In(UNIT_OPTS), + vol.Optional(CONF_UNIT_LIGHTNING, + default=CONF_UNIT_SYSTEM_IMPERIAL): vol.In(UNIT_OPTS), + vol.Optional(CONF_UNIT_WINDCHILL, + default=W_TYPE_HYBRID): vol.In(WINDCHILL_OPTS), + } +) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate user input.""" + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_PORT] == data[CONF_PORT]: + raise AlreadyConfigured + return {"title": f"Ecowitt on port {data[CONF_PORT]}"} + + +class EcowittConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for the Ecowitt.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN + + async def async_step_user(self, user_input=None): + """Give initial instructions for setup.""" + if user_input is not None: + return await self.async_step_initial_options() + + return self.async_show_form(step_id="user") + + async def async_step_initial_options(self, user_input=None): + """Ask the user for the setup options.""" + errors = {} + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + return self.async_create_entry(title=info["title"], + data=user_input) + except AlreadyConfigured: + return self.async_abort(reason="already_configured") + + return self.async_show_form( + step_id="initial_options", data_schema=DATA_SCHEMA, errors=errors + ) + + +class AlreadyConfigured(exceptions.HomeAssistantError): + """Error to indicate this device is already configured.""" diff --git a/custom_components/ecowitt/const.py b/custom_components/ecowitt/const.py index 0b33282..09305f6 100644 --- a/custom_components/ecowitt/const.py +++ b/custom_components/ecowitt/const.py @@ -30,6 +30,8 @@ DEVICE_CLASS_MOISTURE, ) +ECOWITT_PLATFORMS = ["sensor", "binary_sensor"] + TYPE_SENSOR = "sensor" TYPE_BINARY_SENSOR = "binary_sensor" DOMAIN = "ecowitt" @@ -42,6 +44,9 @@ DATA_MODEL = "model" DATA_READY = "ready" +DEFAULT_PORT = 4199 + +CONF_NAME = "component_name" CONF_UNIT_BARO = "barounit" CONF_UNIT_WIND = "windunit" CONF_UNIT_RAIN = "rainunit" diff --git a/custom_components/ecowitt/translations/en.json b/custom_components/ecowitt/translations/en.json new file mode 100644 index 0000000..25c2c2d --- /dev/null +++ b/custom_components/ecowitt/translations/en.json @@ -0,0 +1,26 @@ +{ + "config": { + "title": "Ecowitt Weather Station.", + "abort": { + "already_configured": "Device is already configured." + }, + "step": { + "user": { + "description": "The following steps must be peformed before setting up this integration.\nIf you have not already done so, please do this now.\n\nUse the WS View app (on your phone) for your Ecowitt device, and connect to it.\nPick menu -> device list -> Pick your station.\nHit next repeatedly to move to the last screen titled 'Customized'\n\nPick the protocol Ecowitt, and put in the ip/hostname of your hass server.\nPath doesn't matter as long as it ends in /, leave the default, or change it to just /.\nPick a port that is not in use on the server (netstat -lt). (4199 is probably a good default)\nPick a reasonable value for updates, like 60 seconds.\nSave configuration. The Ecowitt should then start attempting to send data to your server.\n\nClick submit when these instructions have been completed.", + "title": "Instructions for setting up the Ecowitt." + } + "initial_options": { + "title": "Ecowitt Parameters", + "data": { + "name": "Device Name", + "port": "Listening port", + "baro_unit": "Barometer Unit", + "wind_unit": "Wind Unit", + "rain_unit": "Rainfall Unit", + "lightning_unit": "Lightning distance unit", + "windchill_unit": "Windchill calculation", + } + } + } + } +} From f6d791b5073336c0a56063ac491a47fb64b639cd Mon Sep 17 00:00:00 2001 From: garbled1 Date: Fri, 13 Nov 2020 12:25:39 -0700 Subject: [PATCH 03/28] Not sure this is sane either --- custom_components/ecowitt/config_flow.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/custom_components/ecowitt/config_flow.py b/custom_components/ecowitt/config_flow.py index 2ec2809..e8c6b87 100644 --- a/custom_components/ecowitt/config_flow.py +++ b/custom_components/ecowitt/config_flow.py @@ -77,6 +77,14 @@ class EcowittConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN + async def async_step_import(self, device_config): + """Import a configuration.yaml config, if any.""" + + return self.async_create_entry( + title=f"Ecowitt on port {device_config[CONF_PORT]}", + data=device_config + ) + async def async_step_user(self, user_input=None): """Give initial instructions for setup.""" if user_input is not None: From a45cac4ab8d66c95ebf4af07b59215550855f3b4 Mon Sep 17 00:00:00 2001 From: garbled1 Date: Fri, 13 Nov 2020 14:09:13 -0700 Subject: [PATCH 04/28] Debugging --- custom_components/ecowitt/config_flow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/custom_components/ecowitt/config_flow.py b/custom_components/ecowitt/config_flow.py index e8c6b87..f2889c5 100644 --- a/custom_components/ecowitt/config_flow.py +++ b/custom_components/ecowitt/config_flow.py @@ -80,8 +80,10 @@ class EcowittConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, device_config): """Import a configuration.yaml config, if any.""" + port = device_config[CONF_PORT] + _LOGGER.error(device_config) return self.async_create_entry( - title=f"Ecowitt on port {device_config[CONF_PORT]}", + title=f"Ecowitt on port {port}", data=device_config ) From fe9c40fa2ebd7ba4542fd01e2a3f4e1032c40d28 Mon Sep 17 00:00:00 2001 From: garbled1 Date: Fri, 13 Nov 2020 14:10:04 -0700 Subject: [PATCH 05/28] try again --- custom_components/ecowitt/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ecowitt/config_flow.py b/custom_components/ecowitt/config_flow.py index f2889c5..ae9983a 100644 --- a/custom_components/ecowitt/config_flow.py +++ b/custom_components/ecowitt/config_flow.py @@ -80,8 +80,8 @@ class EcowittConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, device_config): """Import a configuration.yaml config, if any.""" - port = device_config[CONF_PORT] _LOGGER.error(device_config) + port = device_config[CONF_PORT] return self.async_create_entry( title=f"Ecowitt on port {port}", data=device_config From 91bc27d517e2c07175442a16164e3063b5acb2b9 Mon Sep 17 00:00:00 2001 From: garbled1 Date: Fri, 13 Nov 2020 14:27:09 -0700 Subject: [PATCH 06/28] pass on import --- custom_components/ecowitt/config_flow.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/custom_components/ecowitt/config_flow.py b/custom_components/ecowitt/config_flow.py index ae9983a..8097938 100644 --- a/custom_components/ecowitt/config_flow.py +++ b/custom_components/ecowitt/config_flow.py @@ -79,13 +79,8 @@ class EcowittConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, device_config): """Import a configuration.yaml config, if any.""" - - _LOGGER.error(device_config) - port = device_config[CONF_PORT] - return self.async_create_entry( - title=f"Ecowitt on port {port}", - data=device_config - ) + # this is broken as hell. It calls it once with each line of the conf. + pass async def async_step_user(self, user_input=None): """Give initial instructions for setup.""" From b8594ae9914b48c175faad7f11b4ac401206bbab Mon Sep 17 00:00:00 2001 From: garbled1 Date: Fri, 13 Nov 2020 14:40:51 -0700 Subject: [PATCH 07/28] Try to just abort out of the config import --- custom_components/ecowitt/config_flow.py | 2 +- custom_components/ecowitt/translations/en.json | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/custom_components/ecowitt/config_flow.py b/custom_components/ecowitt/config_flow.py index 8097938..e8d141b 100644 --- a/custom_components/ecowitt/config_flow.py +++ b/custom_components/ecowitt/config_flow.py @@ -80,7 +80,7 @@ class EcowittConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, device_config): """Import a configuration.yaml config, if any.""" # this is broken as hell. It calls it once with each line of the conf. - pass + return self.async_abort(reason="unknown") async def async_step_user(self, user_input=None): """Give initial instructions for setup.""" diff --git a/custom_components/ecowitt/translations/en.json b/custom_components/ecowitt/translations/en.json index 25c2c2d..fe5d758 100644 --- a/custom_components/ecowitt/translations/en.json +++ b/custom_components/ecowitt/translations/en.json @@ -2,13 +2,14 @@ "config": { "title": "Ecowitt Weather Station.", "abort": { - "already_configured": "Device is already configured." + "already_configured": "Device is already configured.", + "unknown": "Unknown error." }, "step": { "user": { "description": "The following steps must be peformed before setting up this integration.\nIf you have not already done so, please do this now.\n\nUse the WS View app (on your phone) for your Ecowitt device, and connect to it.\nPick menu -> device list -> Pick your station.\nHit next repeatedly to move to the last screen titled 'Customized'\n\nPick the protocol Ecowitt, and put in the ip/hostname of your hass server.\nPath doesn't matter as long as it ends in /, leave the default, or change it to just /.\nPick a port that is not in use on the server (netstat -lt). (4199 is probably a good default)\nPick a reasonable value for updates, like 60 seconds.\nSave configuration. The Ecowitt should then start attempting to send data to your server.\n\nClick submit when these instructions have been completed.", "title": "Instructions for setting up the Ecowitt." - } + }, "initial_options": { "title": "Ecowitt Parameters", "data": { From 090f21d4fe5eda3995209c2510b5499f8b974776 Mon Sep 17 00:00:00 2001 From: garbled1 Date: Fri, 13 Nov 2020 14:45:17 -0700 Subject: [PATCH 08/28] I hate json so much --- custom_components/ecowitt/translations/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ecowitt/translations/en.json b/custom_components/ecowitt/translations/en.json index fe5d758..c5ae2c5 100644 --- a/custom_components/ecowitt/translations/en.json +++ b/custom_components/ecowitt/translations/en.json @@ -19,7 +19,7 @@ "wind_unit": "Wind Unit", "rain_unit": "Rainfall Unit", "lightning_unit": "Lightning distance unit", - "windchill_unit": "Windchill calculation", + "windchill_unit": "Windchill calculation" } } } From d3447c10e37b6fa4ddd1b2749864609be9875eb9 Mon Sep 17 00:00:00 2001 From: garbled1 Date: Fri, 13 Nov 2020 15:07:49 -0700 Subject: [PATCH 09/28] Maybe like this instead? --- custom_components/ecowitt/__init__.py | 9 ++++----- custom_components/ecowitt/config_flow.py | 7 +++++-- custom_components/ecowitt/translations/en.json | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/custom_components/ecowitt/__init__.py b/custom_components/ecowitt/__init__.py index 0a3a543..4fc3dc4 100644 --- a/custom_components/ecowitt/__init__.py +++ b/custom_components/ecowitt/__init__.py @@ -80,12 +80,11 @@ async def async_setup(hass: HomeAssistant, config: dict): hass.data[DOMAIN] = {} if DOMAIN in config: - for entry in config[DOMAIN]: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=entry - ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN] ) + ) return True diff --git a/custom_components/ecowitt/config_flow.py b/custom_components/ecowitt/config_flow.py index e8d141b..e609bf2 100644 --- a/custom_components/ecowitt/config_flow.py +++ b/custom_components/ecowitt/config_flow.py @@ -79,8 +79,11 @@ class EcowittConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, device_config): """Import a configuration.yaml config, if any.""" - # this is broken as hell. It calls it once with each line of the conf. - return self.async_abort(reason="unknown") + port = device_config[CONF_PORT] + return self.async_create_entry( + title=f"Ecowitt on port {port}", + data=device_config + ) async def async_step_user(self, user_input=None): """Give initial instructions for setup.""" diff --git a/custom_components/ecowitt/translations/en.json b/custom_components/ecowitt/translations/en.json index c5ae2c5..aa027c1 100644 --- a/custom_components/ecowitt/translations/en.json +++ b/custom_components/ecowitt/translations/en.json @@ -1,6 +1,6 @@ { "config": { - "title": "Ecowitt Weather Station.", + "title": "Ecowitt Weather Station", "abort": { "already_configured": "Device is already configured.", "unknown": "Unknown error." From bc477468c4beb1ba05fc5257a0f40dca1c38a255 Mon Sep 17 00:00:00 2001 From: garbled1 Date: Fri, 13 Nov 2020 15:10:13 -0700 Subject: [PATCH 10/28] Closer --- custom_components/ecowitt/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/custom_components/ecowitt/__init__.py b/custom_components/ecowitt/__init__.py index 4fc3dc4..a2b0d8f 100644 --- a/custom_components/ecowitt/__init__.py +++ b/custom_components/ecowitt/__init__.py @@ -95,6 +95,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): binary_sensors = [] # Store config + _LOGGER.error(entry.entry_id) + hass.data[DOMAIN][entry.entry_id] = {} hass.data[DOMAIN][entry.entry_id][DATA_STATION] = {} hass.data[DOMAIN][entry.entry_id][DATA_READY] = False From dbff224a285b8e17de068c5b9d4f8b9560348330 Mon Sep 17 00:00:00 2001 From: garbled1 Date: Sun, 15 Nov 2020 08:22:40 -0700 Subject: [PATCH 11/28] Way way closer to something functional here. --- custom_components/ecowitt/__init__.py | 118 +++++++++++------- custom_components/ecowitt/binary_sensor.py | 47 ++++--- custom_components/ecowitt/config_flow.py | 22 ++++ custom_components/ecowitt/const.py | 2 + custom_components/ecowitt/manifest.json | 2 +- custom_components/ecowitt/sensor.py | 53 ++++---- .../ecowitt/translations/en.json | 11 +- 7 files changed, 150 insertions(+), 105 deletions(-) diff --git a/custom_components/ecowitt/__init__.py b/custom_components/ecowitt/__init__.py index a2b0d8f..7a5254f 100644 --- a/custom_components/ecowitt/__init__.py +++ b/custom_components/ecowitt/__init__.py @@ -1,5 +1,5 @@ """The Ecowitt Weather Station Component.""" -# import asyncio +import asyncio import logging import time @@ -52,6 +52,8 @@ W_TYPE_NEW, W_TYPE_OLD, W_TYPE_HYBRID, + REG_ENTITIES, + NEW_ENTITIES, ) _LOGGER = logging.getLogger(__name__) @@ -76,8 +78,9 @@ async def async_setup(hass: HomeAssistant, config: dict): - """Configure the Ecowitt component using flow only.""" - hass.data[DOMAIN] = {} + """Configure the Ecowitt component using YAML.""" + _LOGGER.warning("async_setup") + hass.data.setdefault(DOMAIN, {}) if DOMAIN in config: hass.async_create_task( @@ -89,26 +92,32 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): - """Set up the Ecowitt component from a config entry.""" + """Set up the Ecowitt component from UI.""" - sensor_sensors = [] - binary_sensors = [] + _LOGGER.warning("async_setup_entry") + if hass.data.get(DOMAIN) is None: + hass.data.setdefault(DOMAIN, {}) # Store config _LOGGER.error(entry.entry_id) hass.data[DOMAIN][entry.entry_id] = {} - hass.data[DOMAIN][entry.entry_id][DATA_STATION] = {} - hass.data[DOMAIN][entry.entry_id][DATA_READY] = False + ecowitt_data = hass.data[DOMAIN][entry.entry_id] + ecowitt_data[DATA_STATION] = {} + ecowitt_data[DATA_READY] = False + for et in [REG_ENTITIES, NEW_ENTITIES]: + ecowitt_data[et] = {} + for pl in ECOWITT_PLATFORMS: + ecowitt_data[et][pl] = [] # preload some model info - stationinfo = hass.data[DOMAIN][entry.entry_id][DATA_STATION] + stationinfo = ecowitt_data[DATA_STATION] stationinfo[DATA_STATIONTYPE] = "Unknown" stationinfo[DATA_FREQ] = "Unknown" stationinfo[DATA_MODEL] = "Unknown" # setup the base connection ws = EcoWittListener(port=entry.data[CONF_PORT]) - hass.data[DOMAIN][entry.entry_id][DATA_ECOWITT] = ws + ecowitt_data[DATA_ECOWITT] = ws if entry.data[CONF_UNIT_WINDCHILL] == W_TYPE_OLD: ws.set_windchill(WINDCHILL_OLD) @@ -162,22 +171,19 @@ def check_imp_metric_sensor(sensor): return True def check_and_append_sensor(sensor): - """Check the sensor for validity, and append to appropriate list.""" + """Check the sensor for validity, and append to new entitiy list.""" if sensor not in SENSOR_TYPES: if sensor not in IGNORED_SENSORS: _LOGGER.warning("Unhandled sensor type %s", sensor) - return None + return False # Is this a metric or imperial sensor, lookup and skip if not check_imp_metric_sensor(sensor): - return None + return False name, uom, kind, device_class, icon, metric = SENSOR_TYPES[sensor] - if kind == TYPE_SENSOR: - sensor_sensors.append(sensor) - if kind == TYPE_BINARY_SENSOR: - binary_sensors.append(sensor) - return(kind) + ecowitt_data[NEW_ENTITIES][kind].append(sensor) + return True async def _first_data_rec(weather_data): _LOGGER.info("First ecowitt data recd, setting up sensors.") @@ -200,27 +206,30 @@ async def _first_data_rec(weather_data): # load the sensors we have for sensor in ws.last_values.keys(): + _LOGGER.warning("check sensor " + sensor) check_and_append_sensor(sensor) - if not sensor_sensors and not binary_sensors: + if (not ecowitt_data[NEW_ENTITIES][TYPE_SENSOR] + and not ecowitt_data[NEW_ENTITIES][TYPE_BINARY_SENSOR]): _LOGGER.error("No sensors found to monitor, check device config.") return False - if sensor_sensors: - hass.async_create_task( - async_load_platform(hass, "sensor", DOMAIN, sensor_sensors, - entry.data) - ) - if binary_sensors: + _LOGGER.warning("calling load_platform") + _LOGGER.warning(entry.data) + _LOGGER.warning(entry) + + for component in ECOWITT_PLATFORMS: + _LOGGER.warning("calling fes for " + component) hass.async_create_task( - async_load_platform(hass, "binary_sensor", DOMAIN, binary_sensors, - entry.data) + hass.config_entries.async_forward_entry_setup(entry, component) ) - hass.data[DOMAIN][entry.entry_id][DATA_READY] = True + + ecowitt_data[DATA_READY] = True async def _async_ecowitt_update_cb(weather_data): """Primary update callback called from pyecowitt.""" _LOGGER.debug("Primary update callback triggered.") + _LOGGER.warning("update cb called for id=" + entry.entry_id) if not hass.data[DOMAIN][entry.entry_id][DATA_READY]: await _first_data_rec(weather_data) return @@ -229,44 +238,57 @@ async def _async_ecowitt_update_cb(weather_data): if sensor not in IGNORED_SENSORS: _LOGGER.warning("Unhandled sensor type %s value %s, " + "file a PR.", sensor, weather_data[sensor]) - elif (sensor not in sensor_sensors - and sensor not in binary_sensors + elif (sensor not in ecowitt_data[REG_ENTITIES][TYPE_SENSOR] + and sensor not in ecowitt_data[REG_ENTITIES][TYPE_BINARY_SENSOR] and sensor not in IGNORED_SENSORS and check_imp_metric_sensor(sensor)): _LOGGER.warning("Unregistered sensor type %s value %s received.", sensor, weather_data[sensor]) # try to register the sensor - new_sensor = [] - new_sensor.append(sensor) - kind = check_and_append_sensor(sensor) - if kind == TYPE_SENSOR: - hass.async_create_task( - async_load_platform(hass, "sensor", DOMAIN, - new_sensor, entry.data) - ) - if kind == TYPE_BINARY_SENSOR: - hass.async_create_task( - async_load_platform(hass, "binary_sensor", DOMAIN, - new_sensor, entry.data) - ) - + if check_and_append_sensor(sensor): + for component in ECOWITT_PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup( + entry, component + ) + ) async_dispatcher_send(hass, DOMAIN) + # this is part of the base async_setup_entry ws.register_listener(_async_ecowitt_update_cb) - return True +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + + ws = hass.data[DOMAIN][entry.entry_id][DATA_ECOWITT] + await ws.stop() + + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in ECOWITT_PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + class EcowittEntity(Entity): """Base class for Ecowitt Weather Station.""" - def __init__(self, hass, key, name): + def __init__(self, hass, entry, key, name): """Construct the entity.""" self.hass = hass self._key = key self._name = name - self._stationinfo = hass.data[DOMAIN][DATA_STATION] - self._ws = hass.data[DOMAIN][DATA_ECOWITT] + self._stationinfo = hass.data[DOMAIN][entry.entry_id][DATA_STATION] + self._ws = hass.data[DOMAIN][entry.entry_id][DATA_ECOWITT] @property def should_poll(self): diff --git a/custom_components/ecowitt/binary_sensor.py b/custom_components/ecowitt/binary_sensor.py index 4af75c6..d585b52 100644 --- a/custom_components/ecowitt/binary_sensor.py +++ b/custom_components/ecowitt/binary_sensor.py @@ -1,44 +1,43 @@ """Support for Ecowitt Weather Stations.""" import logging -from . import ( +from . import EcowittEntity +from .const import ( + DOMAIN, TYPE_BINARY_SENSOR, SENSOR_TYPES, - EcowittEntity, + REG_ENTITIES, + NEW_ENTITIES, ) -_LOGGER = logging.getLogger(__name__) - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Setup a single Ecowitt sensor.""" +_LOGGER = logging.getLogger(__name__) - if not discovery_info: - return +async def async_setup_entry(hass, entry, async_add_entities): + """Add sensors if new.""" + ecowitt_data = hass.data[DOMAIN][entry.entry_id] + new_ent = ecowitt_data[NEW_ENTITIES][TYPE_BINARY_SENSOR] + reg_ent = ecowitt_data[REG_ENTITIES][TYPE_BINARY_SENSOR] entities = [] - for sensor in discovery_info: - name, uom, kind, device_class, icon, metric = SENSOR_TYPES[sensor] - if kind == TYPE_BINARY_SENSOR: - entities.append( - EcowittBinarySensor( - hass, - sensor, - name, - device_class, - uom, - icon, - ) - ) + + for new_entity in new_ent: + if new_entity in reg_ent: + continue + reg_ent.append(new_entity) + name, uom, kind, device_class, icon, metric = SENSOR_TYPES[new_entity] + entities.append(EcowittBinarySensor(hass, entry, new_entity, name, + device_class, uom, icon)) + # clear the list + new_ent = [] async_add_entities(entities, True) class EcowittBinarySensor(EcowittEntity): - def __init__(self, hass, key, name, dc, uom, icon): + def __init__(self, hass, entry, key, name, dc, uom, icon): """Initialize the sensor.""" - super().__init__(hass, key, name) + super().__init__(hass, entry, key, name) self._icon = icon self._uom = uom self._dc = dc diff --git a/custom_components/ecowitt/config_flow.py b/custom_components/ecowitt/config_flow.py index e609bf2..69a4bc8 100644 --- a/custom_components/ecowitt/config_flow.py +++ b/custom_components/ecowitt/config_flow.py @@ -61,11 +61,28 @@ default=W_TYPE_HYBRID): vol.In(WINDCHILL_OPTS), } ) +OPTIONS_SCHEMA = vol.Schema( + { + vol.Optional(CONF_UNIT_BARO, + default=CONF_UNIT_SYSTEM_METRIC): vol.In(UNIT_OPTS), + vol.Optional(CONF_UNIT_WIND, + default=CONF_UNIT_SYSTEM_IMPERIAL): vol.In(UNIT_OPTS), + vol.Optional(CONF_UNIT_RAIN, + default=CONF_UNIT_SYSTEM_IMPERIAL): vol.In(UNIT_OPTS), + vol.Optional(CONF_UNIT_LIGHTNING, + default=CONF_UNIT_SYSTEM_IMPERIAL): vol.In(UNIT_OPTS), + vol.Optional(CONF_UNIT_WINDCHILL, + default=W_TYPE_HYBRID): vol.In(WINDCHILL_OPTS), + } +) async def validate_input(hass: core.HomeAssistant, data): """Validate user input.""" for entry in hass.config_entries.async_entries(DOMAIN): + _LOGGER.warning(entry.data[CONF_PORT]) + _LOGGER.warning(data[CONF_PORT]) + _LOGGER.warning(data) if entry.data[CONF_PORT] == data[CONF_PORT]: raise AlreadyConfigured return {"title": f"Ecowitt on port {data[CONF_PORT]}"} @@ -79,6 +96,11 @@ class EcowittConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, device_config): """Import a configuration.yaml config, if any.""" + try: + await validate_input(self.hass, device_config) + except AlreadyConfigured: + return self.async_abort(reason="already_configured") + port = device_config[CONF_PORT] return self.async_create_entry( title=f"Ecowitt on port {port}", diff --git a/custom_components/ecowitt/const.py b/custom_components/ecowitt/const.py index 09305f6..74b5922 100644 --- a/custom_components/ecowitt/const.py +++ b/custom_components/ecowitt/const.py @@ -43,6 +43,8 @@ DATA_FREQ = "freq" DATA_MODEL = "model" DATA_READY = "ready" +REG_ENTITIES = "registered" +NEW_ENTITIES = "new" DEFAULT_PORT = 4199 diff --git a/custom_components/ecowitt/manifest.json b/custom_components/ecowitt/manifest.json index c34fd21..4423058 100644 --- a/custom_components/ecowitt/manifest.json +++ b/custom_components/ecowitt/manifest.json @@ -1,7 +1,7 @@ { "domain": "ecowitt", "name": "Ecowitt Weather Station", - "config_flow": false, + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ecowitt", "requirements": [ "pyecowitt==0.10" diff --git a/custom_components/ecowitt/sensor.py b/custom_components/ecowitt/sensor.py index a86bc84..44f46bd 100644 --- a/custom_components/ecowitt/sensor.py +++ b/custom_components/ecowitt/sensor.py @@ -2,47 +2,48 @@ import logging import homeassistant.util.dt as dt_util -from . import ( +from . import EcowittEntity +from .const import ( + DOMAIN, TYPE_SENSOR, SENSOR_TYPES, - EcowittEntity, - DEVICE_CLASS_TIMESTAMP + REG_ENTITIES, + NEW_ENTITIES, ) -from homeassistant.const import STATE_UNKNOWN +from homeassistant.const import ( + STATE_UNKNOWN, + DEVICE_CLASS_TIMESTAMP +) _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Setup a single Ecowitt sensor.""" - - if not discovery_info: - return - +async def async_setup_entry(hass, entry, async_add_entities): + """Add sensors if new.""" + _LOGGER.warning("sensor setup entry called") + ecowitt_data = hass.data[DOMAIN][entry.entry_id] + new_ent = ecowitt_data[NEW_ENTITIES][TYPE_SENSOR] + reg_ent = ecowitt_data[REG_ENTITIES][TYPE_SENSOR] entities = [] - for sensor in discovery_info: - name, uom, kind, device_class, icon, metric = SENSOR_TYPES[sensor] - if kind == TYPE_SENSOR: - entities.append( - EcowittSensor( - hass, - sensor, - name, - device_class, - uom, - icon, - ) - ) + + for new_entity in new_ent: + if new_entity in reg_ent: + continue + reg_ent.append(new_entity) + name, uom, kind, device_class, icon, metric = SENSOR_TYPES[new_entity] + entities.append(EcowittSensor(hass, entry, new_entity, name, + device_class, uom, icon)) + # clear the list + new_ent = [] async_add_entities(entities, True) class EcowittSensor(EcowittEntity): - def __init__(self, hass, key, name, dc, uom, icon): + def __init__(self, hass, entry, key, name, dc, uom, icon): """Initialize the sensor.""" - super().__init__(hass, key, name) + super().__init__(hass, entry, key, name) self._icon = icon self._uom = uom self._dc = dc diff --git a/custom_components/ecowitt/translations/en.json b/custom_components/ecowitt/translations/en.json index aa027c1..a09708e 100644 --- a/custom_components/ecowitt/translations/en.json +++ b/custom_components/ecowitt/translations/en.json @@ -13,13 +13,12 @@ "initial_options": { "title": "Ecowitt Parameters", "data": { - "name": "Device Name", "port": "Listening port", - "baro_unit": "Barometer Unit", - "wind_unit": "Wind Unit", - "rain_unit": "Rainfall Unit", - "lightning_unit": "Lightning distance unit", - "windchill_unit": "Windchill calculation" + "barounit": "Barometer Unit", + "windunit": "Wind Unit", + "rainunit": "Rainfall Unit", + "lightningunit": "Lightning distance unit", + "windchillunit": "Windchill calculation" } } } From d508c12946651f464f783f01139f41dd511a1ef4 Mon Sep 17 00:00:00 2001 From: garbled1 Date: Sun, 15 Nov 2020 11:52:32 -0700 Subject: [PATCH 12/28] Again, much closer to sanity --- custom_components/ecowitt/__init__.py | 68 ++++++++--- custom_components/ecowitt/config_flow.py | 112 ++++++++++-------- custom_components/ecowitt/const.py | 7 ++ custom_components/ecowitt/schemas.py | 46 +++++++ custom_components/ecowitt/sensor.py | 2 +- .../ecowitt/translations/en.json | 10 +- 6 files changed, 175 insertions(+), 70 deletions(-) create mode 100644 custom_components/ecowitt/schemas.py diff --git a/custom_components/ecowitt/__init__.py b/custom_components/ecowitt/__init__.py index 7a5254f..9453fd7 100644 --- a/custom_components/ecowitt/__init__.py +++ b/custom_components/ecowitt/__init__.py @@ -13,7 +13,6 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import async_load_platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -33,8 +32,8 @@ CONF_UNIT_RAIN, CONF_UNIT_WINDCHILL, CONF_UNIT_LIGHTNING, + CONF_NAME, DOMAIN, - DATA_CONFIG, DATA_ECOWITT, DATA_STATION, DATA_PASSKEY, @@ -42,6 +41,7 @@ DATA_FREQ, DATA_MODEL, DATA_READY, + DATA_OPTIONS, ECOWITT_PLATFORMS, IGNORED_SENSORS, S_IMPERIAL, @@ -83,9 +83,21 @@ async def async_setup(hass: HomeAssistant, config: dict): hass.data.setdefault(DOMAIN, {}) if DOMAIN in config: + data = { + CONF_PORT: config[DOMAIN][CONF_PORT], + CONF_NAME: None, + } + # set the options for migration + hass.data[DOMAIN][DATA_OPTIONS] = { + CONF_UNIT_BARO: config[DOMAIN][CONF_UNIT_BARO], + CONF_UNIT_WIND: config[DOMAIN][CONF_UNIT_WIND], + CONF_UNIT_RAIN: config[DOMAIN][CONF_UNIT_RAIN], + CONF_UNIT_LIGHTNING: config[DOMAIN][CONF_UNIT_LIGHTNING], + CONF_UNIT_WINDCHILL: config[DOMAIN][CONF_UNIT_WINDCHILL], + } hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN] + DOMAIN, context={"source": SOURCE_IMPORT}, data=data ) ) return True @@ -98,6 +110,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if hass.data.get(DOMAIN) is None: hass.data.setdefault(DOMAIN, {}) + # if options existed in the YAML but not in the config entry, add + if (not entry.options + and entry.source == SOURCE_IMPORT + and hass.data.get(DOMAIN) + and hass.data[DOMAIN].get(DATA_OPTIONS)): + hass.config_entries.async_update_entry( + entry=entry, + options=hass.data[DOMAIN][DATA_OPTIONS], + ) + # Store config _LOGGER.error(entry.entry_id) hass.data[DOMAIN][entry.entry_id] = {} @@ -109,6 +131,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): for pl in ECOWITT_PLATFORMS: ecowitt_data[et][pl] = [] + if not entry.options: + entry.options = { + CONF_UNIT_BARO: CONF_UNIT_SYSTEM_METRIC, + CONF_UNIT_WIND: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_RAIN: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_LIGHTNING: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_WINDCHILL: W_TYPE_HYBRID, + } + # preload some model info stationinfo = ecowitt_data[DATA_STATION] stationinfo[DATA_STATIONTYPE] = "Unknown" @@ -119,11 +150,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ws = EcoWittListener(port=entry.data[CONF_PORT]) ecowitt_data[DATA_ECOWITT] = ws - if entry.data[CONF_UNIT_WINDCHILL] == W_TYPE_OLD: + if entry.options[CONF_UNIT_WINDCHILL] == W_TYPE_OLD: ws.set_windchill(WINDCHILL_OLD) - if entry.data[CONF_UNIT_WINDCHILL] == W_TYPE_NEW: + if entry.options[CONF_UNIT_WINDCHILL] == W_TYPE_NEW: ws.set_windchill(WINDCHILL_NEW) - if entry.data[CONF_UNIT_WINDCHILL] == W_TYPE_HYBRID: + if entry.options[CONF_UNIT_WINDCHILL] == W_TYPE_HYBRID: ws.set_windchill(WINDCHILL_HYBRID) hass.loop.create_task(ws.listen()) @@ -142,31 +173,31 @@ def check_imp_metric_sensor(sensor): # Is this a metric or imperial sensor, lookup and skip name, uom, kind, device_class, icon, metric = SENSOR_TYPES[sensor] if "baro" in sensor: - if (entry.data[CONF_UNIT_BARO] == CONF_UNIT_SYSTEM_IMPERIAL + if (entry.options[CONF_UNIT_BARO] == CONF_UNIT_SYSTEM_IMPERIAL and metric == S_METRIC): return False - if (entry.data[CONF_UNIT_BARO] == CONF_UNIT_SYSTEM_METRIC + if (entry.options[CONF_UNIT_BARO] == CONF_UNIT_SYSTEM_METRIC and metric == S_IMPERIAL): return False if "rain" in sensor: - if (entry.data[CONF_UNIT_RAIN] == CONF_UNIT_SYSTEM_IMPERIAL + if (entry.options[CONF_UNIT_RAIN] == CONF_UNIT_SYSTEM_IMPERIAL and metric == S_METRIC): return False - if (entry.data[CONF_UNIT_RAIN] == CONF_UNIT_SYSTEM_METRIC + if (entry.options[CONF_UNIT_RAIN] == CONF_UNIT_SYSTEM_METRIC and metric == S_IMPERIAL): return False if "wind" in sensor: - if (entry.data[CONF_UNIT_WIND] == CONF_UNIT_SYSTEM_IMPERIAL + if (entry.options[CONF_UNIT_WIND] == CONF_UNIT_SYSTEM_IMPERIAL and metric == S_METRIC): return False - if (entry.data[CONF_UNIT_WIND] == CONF_UNIT_SYSTEM_METRIC + if (entry.options[CONF_UNIT_WIND] == CONF_UNIT_SYSTEM_METRIC and metric == S_IMPERIAL): return False if (sensor == 'lightning' - and entry.data[CONF_UNIT_LIGHTNING] == CONF_UNIT_SYSTEM_IMPERIAL): + and entry.options[CONF_UNIT_LIGHTNING] == CONF_UNIT_SYSTEM_IMPERIAL): return False if (sensor == 'lightning_mi' - and entry.data[CONF_UNIT_LIGHTNING] == CONF_UNIT_SYSTEM_METRIC): + and entry.options[CONF_UNIT_LIGHTNING] == CONF_UNIT_SYSTEM_METRIC): return False return True @@ -308,10 +339,15 @@ def name(self): @property def device_info(self): """Return device information for this sensor.""" + _LOGGER.warning("devinfo called") return { - "station": self._stationinfo[DATA_STATIONTYPE], + "identifiers": {(DOMAIN, self._stationinfo[DATA_PASSKEY])}, + "name": self.entry.data[CONF_NAME], + "manufacturer": DOMAIN, "model": self._stationinfo[DATA_MODEL], - "frequency": self._stationinfo[DATA_FREQ], + "sw_version": self._stationinfo[DATA_STATIONTYPE], + "via_device": (DOMAIN, self._stationinfo[DATA_STATIONTYPE]), + # "frequency": self._stationinfo[DATA_FREQ], } async def async_added_to_hass(self): diff --git a/custom_components/ecowitt/config_flow.py b/custom_components/ecowitt/config_flow.py index 69a4bc8..25a85a0 100644 --- a/custom_components/ecowitt/config_flow.py +++ b/custom_components/ecowitt/config_flow.py @@ -1,16 +1,9 @@ """Config flow for ecowitt.""" import logging -from pyecowitt import ( - EcoWittListener, - WINDCHILL_OLD, - WINDCHILL_NEW, - WINDCHILL_HYBRID, -) - import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant import config_entries, core, exceptions +from homeassistant.core import callback from homeassistant.const import ( CONF_PORT, @@ -19,63 +12,24 @@ ) from .const import ( - CONF_NAME, CONF_UNIT_BARO, CONF_UNIT_WIND, CONF_UNIT_RAIN, CONF_UNIT_WINDCHILL, CONF_UNIT_LIGHTNING, - DEFAULT_PORT, DOMAIN, - DATA_CONFIG, - DATA_ECOWITT, - DATA_STATION, - DATA_PASSKEY, - DATA_STATIONTYPE, - DATA_FREQ, - DATA_MODEL, - DATA_READY, - W_TYPE_NEW, - W_TYPE_OLD, W_TYPE_HYBRID, + UNIT_OPTS, + WINDCHILL_OPTS ) -UNIT_OPTS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL] -WINDCHILL_OPTS = [W_TYPE_HYBRID, W_TYPE_NEW, W_TYPE_OLD] +from .schemas import ( + DATA_SCHEMA, +) _LOGGER = logging.getLogger(__name__) -DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_UNIT_BARO, - default=CONF_UNIT_SYSTEM_METRIC): vol.In(UNIT_OPTS), - vol.Optional(CONF_UNIT_WIND, - default=CONF_UNIT_SYSTEM_IMPERIAL): vol.In(UNIT_OPTS), - vol.Optional(CONF_UNIT_RAIN, - default=CONF_UNIT_SYSTEM_IMPERIAL): vol.In(UNIT_OPTS), - vol.Optional(CONF_UNIT_LIGHTNING, - default=CONF_UNIT_SYSTEM_IMPERIAL): vol.In(UNIT_OPTS), - vol.Optional(CONF_UNIT_WINDCHILL, - default=W_TYPE_HYBRID): vol.In(WINDCHILL_OPTS), - } -) -OPTIONS_SCHEMA = vol.Schema( - { - vol.Optional(CONF_UNIT_BARO, - default=CONF_UNIT_SYSTEM_METRIC): vol.In(UNIT_OPTS), - vol.Optional(CONF_UNIT_WIND, - default=CONF_UNIT_SYSTEM_IMPERIAL): vol.In(UNIT_OPTS), - vol.Optional(CONF_UNIT_RAIN, - default=CONF_UNIT_SYSTEM_IMPERIAL): vol.In(UNIT_OPTS), - vol.Optional(CONF_UNIT_LIGHTNING, - default=CONF_UNIT_SYSTEM_IMPERIAL): vol.In(UNIT_OPTS), - vol.Optional(CONF_UNIT_WINDCHILL, - default=W_TYPE_HYBRID): vol.In(WINDCHILL_OPTS), - } -) - async def validate_input(hass: core.HomeAssistant, data): """Validate user input.""" @@ -129,6 +83,60 @@ async def async_step_initial_options(self, user_input=None): step_id="initial_options", data_schema=DATA_SCHEMA, errors=errors ) + @staticmethod + @callback + def async_get_options_flow(config_entry): + return EcowittOptionsFlowHandler(config_entry) + class AlreadyConfigured(exceptions.HomeAssistantError): """Error to indicate this device is already configured.""" + + +class EcowittOptionsFlowHandler(config_entries.OptionsFlow): + """Ecowitt config flow options handler.""" + + def __init__(self, config_entry): + """Initialize HASS options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Handle a flow initialized by the user.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options_schema = vol.Schema( + { + vol.Optional( + CONF_UNIT_BARO, + default=self.config_entry.options.get( + CONF_UNIT_BARO, CONF_UNIT_SYSTEM_METRIC, + ), + ): vol.In(UNIT_OPTS), + vol.Optional( + CONF_UNIT_WIND, + default=self.config_entry.options.get( + CONF_UNIT_WIND, CONF_UNIT_SYSTEM_IMPERIAL, + ), + ): vol.In(UNIT_OPTS), + vol.Optional( + CONF_UNIT_RAIN, + default=self.config_entry.options.get( + CONF_UNIT_RAIN, CONF_UNIT_SYSTEM_IMPERIAL, + ), + ): vol.In(UNIT_OPTS), + vol.Optional( + CONF_UNIT_LIGHTNING, + default=self.config_entry.options.get( + CONF_UNIT_LIGHTNING, CONF_UNIT_SYSTEM_IMPERIAL, + ), + ): vol.In(UNIT_OPTS), + vol.Optional( + CONF_UNIT_WINDCHILL, + default=self.config_entry.options.get( + CONF_UNIT_WINDCHILL, W_TYPE_HYBRID, + ), + ): vol.In(WINDCHILL_OPTS), + } + ) + return self.async_show_form(step_id="init", data_schema=options_schema) diff --git a/custom_components/ecowitt/const.py b/custom_components/ecowitt/const.py index 74b5922..98dd102 100644 --- a/custom_components/ecowitt/const.py +++ b/custom_components/ecowitt/const.py @@ -1,6 +1,8 @@ """Constants used by ecowitt component.""" from homeassistant.const import ( + CONF_UNIT_SYSTEM_METRIC, + CONF_UNIT_SYSTEM_IMPERIAL, DEGREE, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, POWER_WATT, @@ -36,6 +38,7 @@ TYPE_BINARY_SENSOR = "binary_sensor" DOMAIN = "ecowitt" DATA_CONFIG = "config" +DATA_OPTIONS = "options" DATA_ECOWITT = "ecowitt_listener" DATA_STATION = "station" DATA_PASSKEY = "PASSKEY" @@ -191,6 +194,10 @@ LEAK_DETECTED = "Leak Detected" +UNIT_OPTS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL] +WINDCHILL_OPTS = [W_TYPE_HYBRID, W_TYPE_NEW, W_TYPE_OLD] + + # Name, unit_of_measure, type, device_class, icon, metric=1 # name, uom, kind, device_class, icon, metric = SENSOR_TYPES[x] SENSOR_TYPES = { diff --git a/custom_components/ecowitt/schemas.py b/custom_components/ecowitt/schemas.py new file mode 100644 index 0000000..8cb5a5d --- /dev/null +++ b/custom_components/ecowitt/schemas.py @@ -0,0 +1,46 @@ +"""Schema definitions""" +import voluptuous as vol +import homeassistant.helpers.config_validation as cv + +from .const import ( + CONF_UNIT_BARO, + CONF_UNIT_WIND, + CONF_UNIT_RAIN, + CONF_UNIT_WINDCHILL, + CONF_UNIT_LIGHTNING, + CONF_NAME, + DEFAULT_PORT, + DOMAIN, + W_TYPE_HYBRID, +) +from homeassistant.const import ( + CONF_PORT, + CONF_UNIT_SYSTEM_METRIC, + CONF_UNIT_SYSTEM_IMPERIAL, +) + +COMPONENT_SCHEMA = vol.Schema( + { + vol.Required(CONF_PORT): cv.port, + vol.Optional(CONF_UNIT_BARO, + default=CONF_UNIT_SYSTEM_METRIC): cv.string, + vol.Optional(CONF_UNIT_WIND, + default=CONF_UNIT_SYSTEM_IMPERIAL): cv.string, + vol.Optional(CONF_UNIT_RAIN, + default=CONF_UNIT_SYSTEM_IMPERIAL): cv.string, + vol.Optional(CONF_UNIT_LIGHTNING, + default=CONF_UNIT_SYSTEM_IMPERIAL): cv.string, + vol.Optional(CONF_UNIT_WINDCHILL, + default=W_TYPE_HYBRID): cv.string, + } +) + +CONFIG_SCHEMA = vol.Schema({DOMAIN: COMPONENT_SCHEMA}, extra=vol.ALLOW_EXTRA) + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME, + description={"suggested_value": "ecowitt"}): str, + } +) diff --git a/custom_components/ecowitt/sensor.py b/custom_components/ecowitt/sensor.py index 44f46bd..3699c66 100644 --- a/custom_components/ecowitt/sensor.py +++ b/custom_components/ecowitt/sensor.py @@ -73,6 +73,6 @@ def icon(self): return self._icon @property - def device_info(self): + def device_class(self): """Return the device class.""" return self._dc diff --git a/custom_components/ecowitt/translations/en.json b/custom_components/ecowitt/translations/en.json index a09708e..a52b529 100644 --- a/custom_components/ecowitt/translations/en.json +++ b/custom_components/ecowitt/translations/en.json @@ -13,7 +13,15 @@ "initial_options": { "title": "Ecowitt Parameters", "data": { - "port": "Listening port", + "port": "Listening port" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { "barounit": "Barometer Unit", "windunit": "Wind Unit", "rainunit": "Rainfall Unit", From a898a74b138bc6784b826985c14ed7fa2f054551 Mon Sep 17 00:00:00 2001 From: garbled1 Date: Mon, 16 Nov 2020 09:53:13 -0700 Subject: [PATCH 13/28] Fix the leak detector! --- custom_components/ecowitt/__init__.py | 29 ++++++---------------- custom_components/ecowitt/binary_sensor.py | 9 ++++--- custom_components/ecowitt/sensor.py | 1 + 3 files changed, 15 insertions(+), 24 deletions(-) diff --git a/custom_components/ecowitt/__init__.py b/custom_components/ecowitt/__init__.py index 9453fd7..a3d3bcc 100644 --- a/custom_components/ecowitt/__init__.py +++ b/custom_components/ecowitt/__init__.py @@ -58,24 +58,6 @@ _LOGGER = logging.getLogger(__name__) -COMPONENT_SCHEMA = vol.Schema( - { - vol.Required(CONF_PORT): cv.port, - vol.Optional(CONF_UNIT_BARO, - default=CONF_UNIT_SYSTEM_METRIC): cv.string, - vol.Optional(CONF_UNIT_WIND, - default=CONF_UNIT_SYSTEM_IMPERIAL): cv.string, - vol.Optional(CONF_UNIT_RAIN, - default=CONF_UNIT_SYSTEM_IMPERIAL): cv.string, - vol.Optional(CONF_UNIT_LIGHTNING, - default=CONF_UNIT_SYSTEM_IMPERIAL): cv.string, - vol.Optional(CONF_UNIT_WINDCHILL, - default=W_TYPE_HYBRID): cv.string, - } -) - -CONFIG_SCHEMA = vol.Schema({DOMAIN: COMPONENT_SCHEMA}, extra=vol.ALLOW_EXTRA) - async def async_setup(hass: HomeAssistant, config: dict): """Configure the Ecowitt component using YAML.""" @@ -320,6 +302,7 @@ def __init__(self, hass, entry, key, name): self._name = name self._stationinfo = hass.data[DOMAIN][entry.entry_id][DATA_STATION] self._ws = hass.data[DOMAIN][entry.entry_id][DATA_ECOWITT] + self._entry = entry @property def should_poll(self): @@ -339,10 +322,14 @@ def name(self): @property def device_info(self): """Return device information for this sensor.""" - _LOGGER.warning("devinfo called") + if self._entry.data[CONF_NAME] != '': + dname = self._entry.data[CONF_NAME] + else: + dname = "DOMAIN" + return { "identifiers": {(DOMAIN, self._stationinfo[DATA_PASSKEY])}, - "name": self.entry.data[CONF_NAME], + "name": dname, "manufacturer": DOMAIN, "model": self._stationinfo[DATA_MODEL], "sw_version": self._stationinfo[DATA_STATIONTYPE], @@ -357,7 +344,7 @@ async def async_added_to_hass(self): @callback def _update_callback(self) -> None: """Call from dispatcher when state changes.""" - _LOGGER.debug("Updating state with new data. %s", self._name) + _LOGGER.warning("Updating state with new data. %s", self._name) self.async_schedule_update_ha_state(force_refresh=True) @property diff --git a/custom_components/ecowitt/binary_sensor.py b/custom_components/ecowitt/binary_sensor.py index d585b52..b8518f0 100644 --- a/custom_components/ecowitt/binary_sensor.py +++ b/custom_components/ecowitt/binary_sensor.py @@ -3,13 +3,14 @@ from . import EcowittEntity from .const import ( + DATA_ECOWITT, DOMAIN, TYPE_BINARY_SENSOR, SENSOR_TYPES, REG_ENTITIES, NEW_ENTITIES, ) - +from homeassistant.components.binary_sensor import BinarySensorEntity _LOGGER = logging.getLogger(__name__) @@ -25,6 +26,7 @@ async def async_setup_entry(hass, entry, async_add_entities): if new_entity in reg_ent: continue reg_ent.append(new_entity) + _LOGGER.warning(TYPE_BINARY_SENSOR + " " + new_entity) name, uom, kind, device_class, icon, metric = SENSOR_TYPES[new_entity] entities.append(EcowittBinarySensor(hass, entry, new_entity, name, device_class, uom, icon)) @@ -33,7 +35,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(entities, True) -class EcowittBinarySensor(EcowittEntity): +class EcowittBinarySensor(EcowittEntity, BinarySensorEntity): def __init__(self, hass, entry, key, name, dc, uom, icon): """Initialize the sensor.""" @@ -45,7 +47,8 @@ def __init__(self, hass, entry, key, name, dc, uom, icon): @property def is_on(self): """Return true if the binary sensor is on.""" - if self._key in self._ws_last_values: + _LOGGER.warning("hello sensor") + if self._key in self._ws.last_values: if self._ws.last_values[self._key] > 0: return True else: diff --git a/custom_components/ecowitt/sensor.py b/custom_components/ecowitt/sensor.py index 3699c66..3b55042 100644 --- a/custom_components/ecowitt/sensor.py +++ b/custom_components/ecowitt/sensor.py @@ -51,6 +51,7 @@ def __init__(self, hass, entry, key, name, dc, uom, icon): @property def state(self): """Return the state of the sensor.""" + _LOGGER.warning("sensor: request for " + self._key) if self._key in self._ws.last_values: # The lightning time is reported in UTC, hooray. if self._dc == DEVICE_CLASS_TIMESTAMP: From b99e20cdda67d9879fd41d8aff2a520ef737dd49 Mon Sep 17 00:00:00 2001 From: garbled1 Date: Mon, 16 Nov 2020 17:29:18 -0700 Subject: [PATCH 14/28] Also fix this here. --- custom_components/ecowitt/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ecowitt/manifest.json b/custom_components/ecowitt/manifest.json index 4423058..17bae15 100644 --- a/custom_components/ecowitt/manifest.json +++ b/custom_components/ecowitt/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ecowitt", "requirements": [ - "pyecowitt==0.10" + "pyecowitt==0.11" ], "ssdp": [], "zeroconf": [], From 318da7bc5093fc79509a17edee4cbf59ff505827 Mon Sep 17 00:00:00 2001 From: garbled1 Date: Tue, 17 Nov 2020 04:44:26 -0700 Subject: [PATCH 15/28] Retune all the batteries. Set the binary ones to binary sensors, set the voltage ones to voltage sensors, and leave the rest. --- custom_components/ecowitt/const.py | 125 +++++++++++++++-------------- 1 file changed, 64 insertions(+), 61 deletions(-) diff --git a/custom_components/ecowitt/const.py b/custom_components/ecowitt/const.py index 98dd102..a7a6f42 100644 --- a/custom_components/ecowitt/const.py +++ b/custom_components/ecowitt/const.py @@ -21,11 +21,14 @@ TIME_MONTHS, TIME_YEARS, UV_INDEX, + DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TIMESTAMP, + DEVICE_CLASS_VOLTAGE, + VOLT, ) from homeassistant.components.binary_sensor import ( @@ -415,84 +418,84 @@ DEVICE_CLASS_MOISTURE, "mdi:leak", 0), TYPE_LEAK_CH4: ("Leak Detection 4", LEAK_DETECTED, TYPE_BINARY_SENSOR, DEVICE_CLASS_MOISTURE, "mdi:leak", 0), - TYPE_WH25BATT: ("WH25 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_WH26BATT: ("WH26 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_WH40BATT: ("WH40 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), + TYPE_WH25BATT: ("WH25 Battery", "BATT", TYPE_BINARY_SENSOR, + DEVICE_CLASS_BATTERY, "mdi:battery", 0), + TYPE_WH26BATT: ("WH26 Battery", "BATT", TYPE_BINARY_SENSOR, + DEVICE_CLASS_BATTERY, "mdi:battery", 0), + TYPE_WH40BATT: ("WH40 Battery", VOLT, TYPE_SENSOR, + DEVICE_CLASS_VOLTAGE, "mdi:battery", 0), TYPE_WH57BATT: ("WH57 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_WH65BATT: ("WH65 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_WH68BATT: ("WH68 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_WH80BATT: ("WH80 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_SOILBATT1: ("Soil Moisture 1 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_SOILBATT2: ("Soil Moisture 2 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_SOILBATT3: ("Soil Moisture 3 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_SOILBATT4: ("Soil Moisture 4 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_SOILBATT5: ("Soil Moisture 5 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_SOILBATT6: ("Soil Moisture 6 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_SOILBATT7: ("Soil Moisture 7 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_SOILBATT8: ("Soil Moisture 8 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_BATTERY1: ("Battery 1", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_BATTERY2: ("Battery 2", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_BATTERY3: ("Battery 3", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_BATTERY4: ("Battery 4", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_BATTERY5: ("Battery 5", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_BATTERY6: ("Battery 6", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_BATTERY7: ("Battery 7", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), - TYPE_BATTERY8: ("Battery 8", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), + DEVICE_CLASS_BATTERY, "mdi:battery", 0), + TYPE_WH65BATT: ("WH65 Battery", "BATT", TYPE_BINARY_SENSOR, + DEVICE_CLASS_BATTERY, "mdi:battery", 0), + TYPE_WH68BATT: ("WH68 Battery", VOLT, TYPE_SENSOR, + DEVICE_CLASS_BATTERY, "mdi:battery", 0), + TYPE_WH80BATT: ("WH80 Battery", VOLT, TYPE_SENSOR, + DEVICE_CLASS_BATTERY, "mdi:battery", 0), + TYPE_SOILBATT1: ("Soil Moisture 1 Battery", VOLT, TYPE_SENSOR, + DEVICE_CLASS_VOLTAGE, "mdi:battery", 0), + TYPE_SOILBATT2: ("Soil Moisture 2 Battery", VOLT, TYPE_SENSOR, + DEVICE_CLASS_VOLTAGE, "mdi:battery", 0), + TYPE_SOILBATT3: ("Soil Moisture 3 Battery", VOLT, TYPE_SENSOR, + DEVICE_CLASS_VOLTAGE, "mdi:battery", 0), + TYPE_SOILBATT4: ("Soil Moisture 4 Battery", VOLT, TYPE_SENSOR, + DEVICE_CLASS_VOLTAGE, "mdi:battery", 0), + TYPE_SOILBATT5: ("Soil Moisture 5 Battery", VOLT, TYPE_SENSOR, + DEVICE_CLASS_VOLTAGE, "mdi:battery", 0), + TYPE_SOILBATT6: ("Soil Moisture 6 Battery", VOLT, TYPE_SENSOR, + DEVICE_CLASS_VOLTAGE, "mdi:battery", 0), + TYPE_SOILBATT7: ("Soil Moisture 7 Battery", VOLT, TYPE_SENSOR, + DEVICE_CLASS_VOLTAGE, "mdi:battery", 0), + TYPE_SOILBATT8: ("Soil Moisture 8 Battery", VOLT, TYPE_SENSOR, + DEVICE_CLASS_VOLTAGE, "mdi:battery", 0), + TYPE_BATTERY1: ("Battery 1", "BATT", TYPE_BINARY_SENSOR, + DEVICE_CLASS_BATTERY, "mdi:battery", 0), + TYPE_BATTERY2: ("Battery 2", "BATT", TYPE_BINARY_SENSOR, + DEVICE_CLASS_BATTERY, "mdi:battery", 0), + TYPE_BATTERY3: ("Battery 3", "BATT", TYPE_BINARY_SENSOR, + DEVICE_CLASS_BATTERY, "mdi:battery", 0), + TYPE_BATTERY4: ("Battery 4", "BATT", TYPE_BINARY_SENSOR, + DEVICE_CLASS_BATTERY, "mdi:battery", 0), + TYPE_BATTERY5: ("Battery 5", "BATT", TYPE_BINARY_SENSOR, + DEVICE_CLASS_BATTERY, "mdi:battery", 0), + TYPE_BATTERY6: ("Battery 6", "BATT", TYPE_BINARY_SENSOR, + DEVICE_CLASS_BATTERY, "mdi:battery", 0), + TYPE_BATTERY7: ("Battery 7", "BATT", TYPE_BINARY_SENSOR, + DEVICE_CLASS_BATTERY, "mdi:battery", 0), + TYPE_BATTERY8: ("Battery 8", "BATT", TYPE_BINARY_SENSOR, + DEVICE_CLASS_BATTERY, "mdi:battery", 0), TYPE_PM25BATT1: ("PM2.5 1 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), + DEVICE_CLASS_BATTERY, "mdi:battery", 0), TYPE_PM25BATT2: ("PM2.5 2 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), + DEVICE_CLASS_BATTERY, "mdi:battery", 0), TYPE_PM25BATT3: ("PM2.5 3 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), + DEVICE_CLASS_BATTERY, "mdi:battery", 0), TYPE_PM25BATT4: ("PM2.5 4 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), + DEVICE_CLASS_BATTERY, "mdi:battery", 0), TYPE_PM25BATT5: ("PM2.5 5 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), + DEVICE_CLASS_BATTERY, "mdi:battery", 0), TYPE_PM25BATT6: ("PM2.5 6 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), + DEVICE_CLASS_BATTERY, "mdi:battery", 0), TYPE_PM25BATT7: ("PM2.5 7 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), + DEVICE_CLASS_BATTERY, "mdi:battery", 0), TYPE_PM25BATT8: ("PM2.5 8 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), + DEVICE_CLASS_BATTERY, "mdi:battery", 0), TYPE_LEAKBATT1: ("Leak 1 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), + DEVICE_CLASS_BATTERY, "mdi:battery", 0), TYPE_LEAKBATT2: ("Leak 2 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), + DEVICE_CLASS_BATTERY, "mdi:battery", 0), TYPE_LEAKBATT3: ("Leak 3 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), + DEVICE_CLASS_BATTERY, "mdi:battery", 0), TYPE_LEAKBATT4: ("Leak 4 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), + DEVICE_CLASS_BATTERY, "mdi:battery", 0), TYPE_LEAKBATT5: ("Leak 5 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), + DEVICE_CLASS_BATTERY, "mdi:battery", 0), TYPE_LEAKBATT6: ("Leak 6 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), + DEVICE_CLASS_BATTERY, "mdi:battery", 0), TYPE_LEAKBATT7: ("Leak 7 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), + DEVICE_CLASS_BATTERY, "mdi:battery", 0), TYPE_LEAKBATT8: ("Leak 8 Battery", "BATT", TYPE_SENSOR, - None, "mdi:battery", 0), + DEVICE_CLASS_BATTERY, "mdi:battery", 0), } IGNORED_SENSORS = [ From 6db59000eeb8f45c7d3d41b37f48495a5f7b95d7 Mon Sep 17 00:00:00 2001 From: garbled1 Date: Tue, 17 Nov 2020 04:53:02 -0700 Subject: [PATCH 16/28] Need to set defaults when migrating conf --- custom_components/ecowitt/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/custom_components/ecowitt/__init__.py b/custom_components/ecowitt/__init__.py index a3d3bcc..c724743 100644 --- a/custom_components/ecowitt/__init__.py +++ b/custom_components/ecowitt/__init__.py @@ -69,6 +69,17 @@ async def async_setup(hass: HomeAssistant, config: dict): CONF_PORT: config[DOMAIN][CONF_PORT], CONF_NAME: None, } + # set defaults if not set in conf + if CONF_UNIT_BARO not in config[DOMAIN]: + config[DOMAIN][CONF_UNIT_BARO] = CONF_UNIT_SYSTEM_METRIC + if CONF_UNIT_WIND not in config[DOMAIN]: + config[DOMAIN][CONF_UNIT_WIND] = CONF_UNIT_SYSTEM_IMPERIAL + if CONF_UNIT_RAIN not in config[DOMAIN]: + config[DOMAIN][CONF_UNIT_RAIN] = CONF_UNIT_SYSTEM_IMPERIAL + if CONF_UNIT_LIGHTNING not in config[DOMAIN]: + config[DOMAIN][CONF_UNIT_LIGHTNING] = CONF_UNIT_SYSTEM_IMPERIAL + if CONF_UNIT_WINDCHILL not in config[DOMAIN]: + config[DOMAIN][CONF_UNIT_WINDCHILL] = W_TYPE_HYBRID # set the options for migration hass.data[DOMAIN][DATA_OPTIONS] = { CONF_UNIT_BARO: config[DOMAIN][CONF_UNIT_BARO], From 37d2e898345066cfbe017a8e65bac50c87b32559 Mon Sep 17 00:00:00 2001 From: garbled1 Date: Tue, 17 Nov 2020 05:06:12 -0700 Subject: [PATCH 17/28] DOMAIN not in " --- custom_components/ecowitt/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ecowitt/__init__.py b/custom_components/ecowitt/__init__.py index c724743..5aec0e9 100644 --- a/custom_components/ecowitt/__init__.py +++ b/custom_components/ecowitt/__init__.py @@ -336,7 +336,7 @@ def device_info(self): if self._entry.data[CONF_NAME] != '': dname = self._entry.data[CONF_NAME] else: - dname = "DOMAIN" + dname = DOMAIN return { "identifiers": {(DOMAIN, self._stationinfo[DATA_PASSKEY])}, From 159dfef9a7fdd49680efe68b0c87821a7c64f922 Mon Sep 17 00:00:00 2001 From: garbled1 Date: Tue, 17 Nov 2020 05:09:17 -0700 Subject: [PATCH 18/28] Check for none --- custom_components/ecowitt/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/ecowitt/__init__.py b/custom_components/ecowitt/__init__.py index 5aec0e9..84c749b 100644 --- a/custom_components/ecowitt/__init__.py +++ b/custom_components/ecowitt/__init__.py @@ -333,7 +333,8 @@ def name(self): @property def device_info(self): """Return device information for this sensor.""" - if self._entry.data[CONF_NAME] != '': + if (self._entry.data[CONF_NAME] != '' + and self._entry.data[CONF_NAME] is not None): dname = self._entry.data[CONF_NAME] else: dname = DOMAIN From 999fb7f1addd72eaf4039fe74b8c5b19be5cad31 Mon Sep 17 00:00:00 2001 From: garbled1 Date: Tue, 17 Nov 2020 05:18:49 -0700 Subject: [PATCH 19/28] Just trying this out.. --- custom_components/ecowitt/binary_sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/custom_components/ecowitt/binary_sensor.py b/custom_components/ecowitt/binary_sensor.py index b8518f0..bdf19cd 100644 --- a/custom_components/ecowitt/binary_sensor.py +++ b/custom_components/ecowitt/binary_sensor.py @@ -56,10 +56,10 @@ def is_on(self): self._key) return False - @property - def icon(self): - """Return the icon to use in the fronend.""" - return self._icon + # @property + # def icon(self): + # """Return the icon to use in the fronend.""" + # return self._icon @property def device_class(self): From 0c182ba349a481b8f3beabfb79ad5084c72293f5 Mon Sep 17 00:00:00 2001 From: garbled1 Date: Tue, 17 Nov 2020 21:25:28 -0700 Subject: [PATCH 20/28] I think its mostly functonal now. Todo, leak battery looks wrong to me. --- custom_components/ecowitt/__init__.py | 47 ++++++++++++++++----- custom_components/ecowitt/binary_sensor.py | 48 ++++++++++++++-------- custom_components/ecowitt/const.py | 6 ++- custom_components/ecowitt/sensor.py | 32 +++++++-------- 4 files changed, 89 insertions(+), 44 deletions(-) diff --git a/custom_components/ecowitt/__init__.py b/custom_components/ecowitt/__init__.py index 84c749b..d67df23 100644 --- a/custom_components/ecowitt/__init__.py +++ b/custom_components/ecowitt/__init__.py @@ -9,10 +9,8 @@ WINDCHILL_NEW, WINDCHILL_HYBRID, ) -import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -import homeassistant.helpers.config_validation as cv from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -54,6 +52,7 @@ W_TYPE_HYBRID, REG_ENTITIES, NEW_ENTITIES, + SIGNAL_ADD_ENTITIES, ) _LOGGER = logging.getLogger(__name__) @@ -206,7 +205,7 @@ def check_and_append_sensor(sensor): return False name, uom, kind, device_class, icon, metric = SENSOR_TYPES[sensor] - ecowitt_data[NEW_ENTITIES][kind].append(sensor) + ecowitt_data[REG_ENTITIES][kind].append(sensor) return True async def _first_data_rec(weather_data): @@ -233,8 +232,8 @@ async def _first_data_rec(weather_data): _LOGGER.warning("check sensor " + sensor) check_and_append_sensor(sensor) - if (not ecowitt_data[NEW_ENTITIES][TYPE_SENSOR] - and not ecowitt_data[NEW_ENTITIES][TYPE_BINARY_SENSOR]): + if (not ecowitt_data[REG_ENTITIES][TYPE_SENSOR] + and not ecowitt_data[REG_ENTITIES][TYPE_BINARY_SENSOR]): _LOGGER.error("No sensors found to monitor, check device config.") return False @@ -247,6 +246,11 @@ async def _first_data_rec(weather_data): hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) ) + # signal = f"{SIGNAL_ADD_ENTITIES}_{component}" + # async_dispatcher_send(hass, signal, + # ecowitt_data[REG_ENTITIES][component]) + # _LOGGER.warning("called signal for component " + component) + # _LOGGER.warning(ecowitt_data[REG_ENTITIES][component]) ecowitt_data[DATA_READY] = True @@ -254,6 +258,11 @@ async def _async_ecowitt_update_cb(weather_data): """Primary update callback called from pyecowitt.""" _LOGGER.debug("Primary update callback triggered.") _LOGGER.warning("update cb called for id=" + entry.entry_id) + + new_sensors = {} + for component in ECOWITT_PLATFORMS: + new_sensors[component] = [] + if not hass.data[DOMAIN][entry.entry_id][DATA_READY]: await _first_data_rec(weather_data) return @@ -270,12 +279,11 @@ async def _async_ecowitt_update_cb(weather_data): sensor, weather_data[sensor]) # try to register the sensor if check_and_append_sensor(sensor): + name, uom, kind, device_class, icon, metric = SENSOR_TYPES[sensor] + new_sensors[kind].append(sensor) for component in ECOWITT_PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup( - entry, component - ) - ) + signal = f"{SIGNAL_ADD_ENTITIES}_{component}" + async_dispatcher_send(hass, signal, new_sensors[kind]) async_dispatcher_send(hass, DOMAIN) # this is part of the base async_setup_entry @@ -303,6 +311,25 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): return unload_ok +def async_add_ecowitt_entities(hass, entry, entity_type, + platform, async_add_entities, + discovery_info): + entities = [] + _LOGGER.warning("Calling async_add_ecowitt_entities") + _LOGGER.warning(discovery_info) + if discovery_info is None: + return + + for new_entity in discovery_info: + if new_entity not in hass.data[DOMAIN][entry.entry_id][REG_ENTITIES][platform]: + hass.data[DOMAIN][entry.entry_id][REG_ENTITIES][platform].append(new_entity) + name, uom, kind, device_class, icon, metric = SENSOR_TYPES[new_entity] + entities.append(entity_type(hass, entry, new_entity, name, + device_class, uom, icon)) + if entities: + async_add_entities(entities, True) + + class EcowittEntity(Entity): """Base class for Ecowitt Weather Station.""" diff --git a/custom_components/ecowitt/binary_sensor.py b/custom_components/ecowitt/binary_sensor.py index bdf19cd..0a243e0 100644 --- a/custom_components/ecowitt/binary_sensor.py +++ b/custom_components/ecowitt/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Ecowitt Weather Stations.""" import logging -from . import EcowittEntity +from . import EcowittEntity, async_add_ecowitt_entities from .const import ( DATA_ECOWITT, DOMAIN, @@ -9,30 +9,46 @@ SENSOR_TYPES, REG_ENTITIES, NEW_ENTITIES, + SIGNAL_ADD_ENTITIES, ) from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.helpers.dispatcher import async_dispatcher_connect _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, entry, async_add_entities): """Add sensors if new.""" - ecowitt_data = hass.data[DOMAIN][entry.entry_id] - new_ent = ecowitt_data[NEW_ENTITIES][TYPE_BINARY_SENSOR] - reg_ent = ecowitt_data[REG_ENTITIES][TYPE_BINARY_SENSOR] - entities = [] - for new_entity in new_ent: - if new_entity in reg_ent: - continue - reg_ent.append(new_entity) - _LOGGER.warning(TYPE_BINARY_SENSOR + " " + new_entity) - name, uom, kind, device_class, icon, metric = SENSOR_TYPES[new_entity] - entities.append(EcowittBinarySensor(hass, entry, new_entity, name, - device_class, uom, icon)) - # clear the list - new_ent = [] - async_add_entities(entities, True) + def add_entities(discovery_info=None): + async_add_ecowitt_entities(hass, entry, EcowittBinarySensor, + BINARY_SENSOR_DOMAIN, async_add_entities, + discovery_info) + + signal = f"{SIGNAL_ADD_ENTITIES}_{BINARY_SENSOR_DOMAIN}" + async_dispatcher_connect(hass, signal, add_entities) + add_entities(hass.data[DOMAIN][entry.entry_id][REG_ENTITIES][TYPE_BINARY_SENSOR]) + + +# async def async_setup_entry(hass, entry, async_add_entities): +# """Add sensors if new.""" +# ecowitt_data = hass.data[DOMAIN][entry.entry_id] +# new_ent = ecowitt_data[NEW_ENTITIES][TYPE_BINARY_SENSOR] +# reg_ent = ecowitt_data[REG_ENTITIES][TYPE_BINARY_SENSOR] +# entities = [] + +# for new_entity in new_ent: +# if new_entity in reg_ent: +# continue +# reg_ent.append(new_entity) +# _LOGGER.warning(TYPE_BINARY_SENSOR + " " + new_entity) +# name, uom, kind, device_class, icon, metric = SENSOR_TYPES[new_entity] +# entities.append(EcowittBinarySensor(hass, entry, new_entity, name, +# device_class, uom, icon)) +# # clear the list +# new_ent = [] +# async_add_entities(entities, True) class EcowittBinarySensor(EcowittEntity, BinarySensorEntity): diff --git a/custom_components/ecowitt/const.py b/custom_components/ecowitt/const.py index a7a6f42..17e813d 100644 --- a/custom_components/ecowitt/const.py +++ b/custom_components/ecowitt/const.py @@ -54,6 +54,8 @@ DEFAULT_PORT = 4199 +SIGNAL_ADD_ENTITIES = "ecowitt_add_entities" + CONF_NAME = "component_name" CONF_UNIT_BARO = "barounit" CONF_UNIT_WIND = "windunit" @@ -279,9 +281,9 @@ TYPE_SENSOR, DEVICE_CLASS_HUMIDITY, "mdi:water-percent", 0), TYPE_WINDDIR: ("Wind Direction", DEGREE, - TYPE_SENSOR, None, "mdi:water-percent", 0), + TYPE_SENSOR, None, "mdi:compass", 0), TYPE_WINDDIR_A10: ("Wind Direction 10m Avg", DEGREE, - TYPE_SENSOR, None, "mdi:water-percent", 0), + TYPE_SENSOR, None, "mdi:compass", 0), TYPE_WINDSPEEDKMH: ("Wind Speed", SPEED_KILOMETERS_PER_HOUR, TYPE_SENSOR, None, "mdi:weather-windy", S_METRIC), TYPE_WINDSPEEDKMH_A10: ("Wind Speed 10m Avg", SPEED_KILOMETERS_PER_HOUR, diff --git a/custom_components/ecowitt/sensor.py b/custom_components/ecowitt/sensor.py index 3b55042..cb95133 100644 --- a/custom_components/ecowitt/sensor.py +++ b/custom_components/ecowitt/sensor.py @@ -2,13 +2,16 @@ import logging import homeassistant.util.dt as dt_util -from . import EcowittEntity +from . import EcowittEntity, async_add_ecowitt_entities +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( DOMAIN, TYPE_SENSOR, SENSOR_TYPES, REG_ENTITIES, NEW_ENTITIES, + SIGNAL_ADD_ENTITIES, ) from homeassistant.const import ( @@ -21,22 +24,19 @@ async def async_setup_entry(hass, entry, async_add_entities): """Add sensors if new.""" - _LOGGER.warning("sensor setup entry called") - ecowitt_data = hass.data[DOMAIN][entry.entry_id] - new_ent = ecowitt_data[NEW_ENTITIES][TYPE_SENSOR] - reg_ent = ecowitt_data[REG_ENTITIES][TYPE_SENSOR] - entities = [] - for new_entity in new_ent: - if new_entity in reg_ent: - continue - reg_ent.append(new_entity) - name, uom, kind, device_class, icon, metric = SENSOR_TYPES[new_entity] - entities.append(EcowittSensor(hass, entry, new_entity, name, - device_class, uom, icon)) - # clear the list - new_ent = [] - async_add_entities(entities, True) + _LOGGER.warning("called async_setup_entry in sensor") + + def add_entities(discovery_info=None): + _LOGGER.warning("called add_entities in sensor") + _LOGGER.warning(discovery_info) + async_add_ecowitt_entities(hass, entry, EcowittSensor, + SENSOR_DOMAIN, async_add_entities, + discovery_info) + + signal = f"{SIGNAL_ADD_ENTITIES}_{SENSOR_DOMAIN}" + async_dispatcher_connect(hass, signal, add_entities) + add_entities(hass.data[DOMAIN][entry.entry_id][REG_ENTITIES][TYPE_SENSOR]) class EcowittSensor(EcowittEntity): From e809ac781a96bc2ee1451d044989938605faa424 Mon Sep 17 00:00:00 2001 From: garbled1 Date: Tue, 17 Nov 2020 21:35:20 -0700 Subject: [PATCH 21/28] Rewire the 0-5 sensor as a percentage of 0-100 by multiplying level by 20 --- custom_components/ecowitt/const.py | 34 ++++++++++++++--------------- custom_components/ecowitt/sensor.py | 6 ++++- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/custom_components/ecowitt/const.py b/custom_components/ecowitt/const.py index 17e813d..be3f216 100644 --- a/custom_components/ecowitt/const.py +++ b/custom_components/ecowitt/const.py @@ -426,7 +426,7 @@ DEVICE_CLASS_BATTERY, "mdi:battery", 0), TYPE_WH40BATT: ("WH40 Battery", VOLT, TYPE_SENSOR, DEVICE_CLASS_VOLTAGE, "mdi:battery", 0), - TYPE_WH57BATT: ("WH57 Battery", "BATT", TYPE_SENSOR, + TYPE_WH57BATT: ("WH57 Battery", PERCENTAGE, TYPE_SENSOR, DEVICE_CLASS_BATTERY, "mdi:battery", 0), TYPE_WH65BATT: ("WH65 Battery", "BATT", TYPE_BINARY_SENSOR, DEVICE_CLASS_BATTERY, "mdi:battery", 0), @@ -466,37 +466,37 @@ DEVICE_CLASS_BATTERY, "mdi:battery", 0), TYPE_BATTERY8: ("Battery 8", "BATT", TYPE_BINARY_SENSOR, DEVICE_CLASS_BATTERY, "mdi:battery", 0), - TYPE_PM25BATT1: ("PM2.5 1 Battery", "BATT", TYPE_SENSOR, + TYPE_PM25BATT1: ("PM2.5 1 Battery", PERCENTAGE, TYPE_SENSOR, DEVICE_CLASS_BATTERY, "mdi:battery", 0), - TYPE_PM25BATT2: ("PM2.5 2 Battery", "BATT", TYPE_SENSOR, + TYPE_PM25BATT2: ("PM2.5 2 Battery", PERCENTAGE, TYPE_SENSOR, DEVICE_CLASS_BATTERY, "mdi:battery", 0), - TYPE_PM25BATT3: ("PM2.5 3 Battery", "BATT", TYPE_SENSOR, + TYPE_PM25BATT3: ("PM2.5 3 Battery", PERCENTAGE, TYPE_SENSOR, DEVICE_CLASS_BATTERY, "mdi:battery", 0), - TYPE_PM25BATT4: ("PM2.5 4 Battery", "BATT", TYPE_SENSOR, + TYPE_PM25BATT4: ("PM2.5 4 Battery", PERCENTAGE, TYPE_SENSOR, DEVICE_CLASS_BATTERY, "mdi:battery", 0), - TYPE_PM25BATT5: ("PM2.5 5 Battery", "BATT", TYPE_SENSOR, + TYPE_PM25BATT5: ("PM2.5 5 Battery", PERCENTAGE, TYPE_SENSOR, DEVICE_CLASS_BATTERY, "mdi:battery", 0), - TYPE_PM25BATT6: ("PM2.5 6 Battery", "BATT", TYPE_SENSOR, + TYPE_PM25BATT6: ("PM2.5 6 Battery", PERCENTAGE, TYPE_SENSOR, DEVICE_CLASS_BATTERY, "mdi:battery", 0), - TYPE_PM25BATT7: ("PM2.5 7 Battery", "BATT", TYPE_SENSOR, + TYPE_PM25BATT7: ("PM2.5 7 Battery", PERCENTAGE, TYPE_SENSOR, DEVICE_CLASS_BATTERY, "mdi:battery", 0), - TYPE_PM25BATT8: ("PM2.5 8 Battery", "BATT", TYPE_SENSOR, + TYPE_PM25BATT8: ("PM2.5 8 Battery", PERCENTAGE, TYPE_SENSOR, DEVICE_CLASS_BATTERY, "mdi:battery", 0), - TYPE_LEAKBATT1: ("Leak 1 Battery", "BATT", TYPE_SENSOR, + TYPE_LEAKBATT1: ("Leak 1 Battery", PERCENTAGE, TYPE_SENSOR, DEVICE_CLASS_BATTERY, "mdi:battery", 0), - TYPE_LEAKBATT2: ("Leak 2 Battery", "BATT", TYPE_SENSOR, + TYPE_LEAKBATT2: ("Leak 2 Battery", PERCENTAGE, TYPE_SENSOR, DEVICE_CLASS_BATTERY, "mdi:battery", 0), - TYPE_LEAKBATT3: ("Leak 3 Battery", "BATT", TYPE_SENSOR, + TYPE_LEAKBATT3: ("Leak 3 Battery", PERCENTAGE, TYPE_SENSOR, DEVICE_CLASS_BATTERY, "mdi:battery", 0), - TYPE_LEAKBATT4: ("Leak 4 Battery", "BATT", TYPE_SENSOR, + TYPE_LEAKBATT4: ("Leak 4 Battery", PERCENTAGE, TYPE_SENSOR, DEVICE_CLASS_BATTERY, "mdi:battery", 0), - TYPE_LEAKBATT5: ("Leak 5 Battery", "BATT", TYPE_SENSOR, + TYPE_LEAKBATT5: ("Leak 5 Battery", PERCENTAGE, TYPE_SENSOR, DEVICE_CLASS_BATTERY, "mdi:battery", 0), - TYPE_LEAKBATT6: ("Leak 6 Battery", "BATT", TYPE_SENSOR, + TYPE_LEAKBATT6: ("Leak 6 Battery", PERCENTAGE, TYPE_SENSOR, DEVICE_CLASS_BATTERY, "mdi:battery", 0), - TYPE_LEAKBATT7: ("Leak 7 Battery", "BATT", TYPE_SENSOR, + TYPE_LEAKBATT7: ("Leak 7 Battery", PERCENTAGE, TYPE_SENSOR, DEVICE_CLASS_BATTERY, "mdi:battery", 0), - TYPE_LEAKBATT8: ("Leak 8 Battery", "BATT", TYPE_SENSOR, + TYPE_LEAKBATT8: ("Leak 8 Battery", PERCENTAGE, TYPE_SENSOR, DEVICE_CLASS_BATTERY, "mdi:battery", 0), } diff --git a/custom_components/ecowitt/sensor.py b/custom_components/ecowitt/sensor.py index cb95133..9411eb4 100644 --- a/custom_components/ecowitt/sensor.py +++ b/custom_components/ecowitt/sensor.py @@ -16,7 +16,9 @@ from homeassistant.const import ( STATE_UNKNOWN, - DEVICE_CLASS_TIMESTAMP + DEVICE_CLASS_TIMESTAMP, + DEVICE_CLASS_BATTERY, + PERCENTAGE, ) _LOGGER = logging.getLogger(__name__) @@ -58,6 +60,8 @@ def state(self): return dt_util.as_local( dt_util.utc_from_timestamp(self._ws.last_values[self._key]) ).isoformat() + if self._dc == DEVICE_CLASS_BATTERY and self._uom == PERCENTAGE: + return self._ws.last_values[self._key] * 20.0 return self._ws.last_values[self._key] _LOGGER.warning("Sensor %s not in last update, check range or battery", self._key) From 14501961bbeef6ffcf86f0774db8317262992699 Mon Sep 17 00:00:00 2001 From: garbled1 Date: Wed, 18 Nov 2020 05:16:10 -0700 Subject: [PATCH 22/28] Start cleaning up for final commit. --- custom_components/ecowitt/__init__.py | 21 +++++++++++----- custom_components/ecowitt/binary_sensor.py | 29 ---------------------- custom_components/ecowitt/const.py | 1 - custom_components/ecowitt/sensor.py | 8 +----- 4 files changed, 16 insertions(+), 43 deletions(-) diff --git a/custom_components/ecowitt/__init__.py b/custom_components/ecowitt/__init__.py index d67df23..9e79aa0 100644 --- a/custom_components/ecowitt/__init__.py +++ b/custom_components/ecowitt/__init__.py @@ -51,16 +51,17 @@ W_TYPE_OLD, W_TYPE_HYBRID, REG_ENTITIES, - NEW_ENTITIES, SIGNAL_ADD_ENTITIES, ) +NOTIFICATION_ID = DOMAIN +NOTIFICATION_TITLE = "Ecowitt config migrated" + _LOGGER = logging.getLogger(__name__) async def async_setup(hass: HomeAssistant, config: dict): """Configure the Ecowitt component using YAML.""" - _LOGGER.warning("async_setup") hass.data.setdefault(DOMAIN, {}) if DOMAIN in config: @@ -87,6 +88,15 @@ async def async_setup(hass: HomeAssistant, config: dict): CONF_UNIT_LIGHTNING: config[DOMAIN][CONF_UNIT_LIGHTNING], CONF_UNIT_WINDCHILL: config[DOMAIN][CONF_UNIT_WINDCHILL], } + hass.components.persistent_notification.create( + "Ecowitt configuration has been migrated from yaml format " + "to a config_flow. Your options and settings should have been " + "migrated automatically. Verify them in the Configuration -> " + "Integrations menu, and then delete the ecowitt section from " + "your yaml file.", + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID, + ) hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=data @@ -118,10 +128,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ecowitt_data = hass.data[DOMAIN][entry.entry_id] ecowitt_data[DATA_STATION] = {} ecowitt_data[DATA_READY] = False - for et in [REG_ENTITIES, NEW_ENTITIES]: - ecowitt_data[et] = {} - for pl in ECOWITT_PLATFORMS: - ecowitt_data[et][pl] = [] + ecowitt_data[REG_ENTITIES] = {} + for pl in ECOWITT_PLATFORMS: + ecowitt_data[REG_ENTITIES][pl] = [] if not entry.options: entry.options = { diff --git a/custom_components/ecowitt/binary_sensor.py b/custom_components/ecowitt/binary_sensor.py index 0a243e0..c83fa9c 100644 --- a/custom_components/ecowitt/binary_sensor.py +++ b/custom_components/ecowitt/binary_sensor.py @@ -3,12 +3,9 @@ from . import EcowittEntity, async_add_ecowitt_entities from .const import ( - DATA_ECOWITT, DOMAIN, TYPE_BINARY_SENSOR, - SENSOR_TYPES, REG_ENTITIES, - NEW_ENTITIES, SIGNAL_ADD_ENTITIES, ) from homeassistant.components.binary_sensor import BinarySensorEntity @@ -31,26 +28,6 @@ def add_entities(discovery_info=None): add_entities(hass.data[DOMAIN][entry.entry_id][REG_ENTITIES][TYPE_BINARY_SENSOR]) -# async def async_setup_entry(hass, entry, async_add_entities): -# """Add sensors if new.""" -# ecowitt_data = hass.data[DOMAIN][entry.entry_id] -# new_ent = ecowitt_data[NEW_ENTITIES][TYPE_BINARY_SENSOR] -# reg_ent = ecowitt_data[REG_ENTITIES][TYPE_BINARY_SENSOR] -# entities = [] - -# for new_entity in new_ent: -# if new_entity in reg_ent: -# continue -# reg_ent.append(new_entity) -# _LOGGER.warning(TYPE_BINARY_SENSOR + " " + new_entity) -# name, uom, kind, device_class, icon, metric = SENSOR_TYPES[new_entity] -# entities.append(EcowittBinarySensor(hass, entry, new_entity, name, -# device_class, uom, icon)) -# # clear the list -# new_ent = [] -# async_add_entities(entities, True) - - class EcowittBinarySensor(EcowittEntity, BinarySensorEntity): def __init__(self, hass, entry, key, name, dc, uom, icon): @@ -63,7 +40,6 @@ def __init__(self, hass, entry, key, name, dc, uom, icon): @property def is_on(self): """Return true if the binary sensor is on.""" - _LOGGER.warning("hello sensor") if self._key in self._ws.last_values: if self._ws.last_values[self._key] > 0: return True @@ -72,11 +48,6 @@ def is_on(self): self._key) return False - # @property - # def icon(self): - # """Return the icon to use in the fronend.""" - # return self._icon - @property def device_class(self): """Return the device class.""" diff --git a/custom_components/ecowitt/const.py b/custom_components/ecowitt/const.py index be3f216..cd5c9a4 100644 --- a/custom_components/ecowitt/const.py +++ b/custom_components/ecowitt/const.py @@ -50,7 +50,6 @@ DATA_MODEL = "model" DATA_READY = "ready" REG_ENTITIES = "registered" -NEW_ENTITIES = "new" DEFAULT_PORT = 4199 diff --git a/custom_components/ecowitt/sensor.py b/custom_components/ecowitt/sensor.py index 9411eb4..6460ee0 100644 --- a/custom_components/ecowitt/sensor.py +++ b/custom_components/ecowitt/sensor.py @@ -8,9 +8,7 @@ from .const import ( DOMAIN, TYPE_SENSOR, - SENSOR_TYPES, REG_ENTITIES, - NEW_ENTITIES, SIGNAL_ADD_ENTITIES, ) @@ -27,11 +25,7 @@ async def async_setup_entry(hass, entry, async_add_entities): """Add sensors if new.""" - _LOGGER.warning("called async_setup_entry in sensor") - def add_entities(discovery_info=None): - _LOGGER.warning("called add_entities in sensor") - _LOGGER.warning(discovery_info) async_add_ecowitt_entities(hass, entry, EcowittSensor, SENSOR_DOMAIN, async_add_entities, discovery_info) @@ -53,13 +47,13 @@ def __init__(self, hass, entry, key, name, dc, uom, icon): @property def state(self): """Return the state of the sensor.""" - _LOGGER.warning("sensor: request for " + self._key) if self._key in self._ws.last_values: # The lightning time is reported in UTC, hooray. if self._dc == DEVICE_CLASS_TIMESTAMP: return dt_util.as_local( dt_util.utc_from_timestamp(self._ws.last_values[self._key]) ).isoformat() + # Battery value is 0-5 if self._dc == DEVICE_CLASS_BATTERY and self._uom == PERCENTAGE: return self._ws.last_values[self._key] * 20.0 return self._ws.last_values[self._key] From 557212c6c4997ba29c8ded09cd8a586ea4aa225b Mon Sep 17 00:00:00 2001 From: garbled1 Date: Wed, 18 Nov 2020 05:28:25 -0700 Subject: [PATCH 23/28] Move this to a lower level --- custom_components/ecowitt/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/custom_components/ecowitt/__init__.py b/custom_components/ecowitt/__init__.py index 9e79aa0..a22b2a8 100644 --- a/custom_components/ecowitt/__init__.py +++ b/custom_components/ecowitt/__init__.py @@ -290,9 +290,10 @@ async def _async_ecowitt_update_cb(weather_data): if check_and_append_sensor(sensor): name, uom, kind, device_class, icon, metric = SENSOR_TYPES[sensor] new_sensors[kind].append(sensor) - for component in ECOWITT_PLATFORMS: - signal = f"{SIGNAL_ADD_ENTITIES}_{component}" - async_dispatcher_send(hass, signal, new_sensors[kind]) + for component in ECOWITT_PLATFORMS: + if new_sensors[component]: + signal = f"{SIGNAL_ADD_ENTITIES}_{component}" + async_dispatcher_send(hass, signal, new_sensors[component]) async_dispatcher_send(hass, DOMAIN) # this is part of the base async_setup_entry From 6b9e4b368219fe9e13649f2d6ef31f6bfda0f317 Mon Sep 17 00:00:00 2001 From: garbled1 Date: Wed, 18 Nov 2020 05:47:56 -0700 Subject: [PATCH 24/28] I think I need to return none if the sensor isn't in the update --- custom_components/ecowitt/__init__.py | 23 +++++++--------------- custom_components/ecowitt/binary_sensor.py | 1 + 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/custom_components/ecowitt/__init__.py b/custom_components/ecowitt/__init__.py index a22b2a8..f53454c 100644 --- a/custom_components/ecowitt/__init__.py +++ b/custom_components/ecowitt/__init__.py @@ -164,11 +164,6 @@ async def close_server(*args): """ Close the ecowitt server.""" await ws.stop() - # hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_server) - - # # go to sleep until we get the first report - # await ws.wait_for_valid_data() - def check_imp_metric_sensor(sensor): """Check if this is the wrong sensor for our config (imp/metric).""" # Is this a metric or imperial sensor, lookup and skip @@ -187,7 +182,7 @@ def check_imp_metric_sensor(sensor): if (entry.options[CONF_UNIT_RAIN] == CONF_UNIT_SYSTEM_METRIC and metric == S_IMPERIAL): return False - if "wind" in sensor: + if "windchill" not in sensor and ("wind" in sensor or "gust" in sensor): if (entry.options[CONF_UNIT_WIND] == CONF_UNIT_SYSTEM_IMPERIAL and metric == S_METRIC): return False @@ -207,15 +202,15 @@ def check_and_append_sensor(sensor): if sensor not in SENSOR_TYPES: if sensor not in IGNORED_SENSORS: _LOGGER.warning("Unhandled sensor type %s", sensor) - return False + return None # Is this a metric or imperial sensor, lookup and skip if not check_imp_metric_sensor(sensor): - return False + return None name, uom, kind, device_class, icon, metric = SENSOR_TYPES[sensor] ecowitt_data[REG_ENTITIES][kind].append(sensor) - return True + return kind async def _first_data_rec(weather_data): _LOGGER.info("First ecowitt data recd, setting up sensors.") @@ -255,11 +250,6 @@ async def _first_data_rec(weather_data): hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) ) - # signal = f"{SIGNAL_ADD_ENTITIES}_{component}" - # async_dispatcher_send(hass, signal, - # ecowitt_data[REG_ENTITIES][component]) - # _LOGGER.warning("called signal for component " + component) - # _LOGGER.warning(ecowitt_data[REG_ENTITIES][component]) ecowitt_data[DATA_READY] = True @@ -287,9 +277,10 @@ async def _async_ecowitt_update_cb(weather_data): _LOGGER.warning("Unregistered sensor type %s value %s received.", sensor, weather_data[sensor]) # try to register the sensor - if check_and_append_sensor(sensor): - name, uom, kind, device_class, icon, metric = SENSOR_TYPES[sensor] + kind = check_and_append_sensor(sensor) + if kind is not None: new_sensors[kind].append(sensor) + # if we have new sensors, set them up. for component in ECOWITT_PLATFORMS: if new_sensors[component]: signal = f"{SIGNAL_ADD_ENTITIES}_{component}" diff --git a/custom_components/ecowitt/binary_sensor.py b/custom_components/ecowitt/binary_sensor.py index c83fa9c..f47dd3a 100644 --- a/custom_components/ecowitt/binary_sensor.py +++ b/custom_components/ecowitt/binary_sensor.py @@ -46,6 +46,7 @@ def is_on(self): else: _LOGGER.warning("Sensor %s not in last update, check range or battery", self._key) + return None return False @property From 08287cdea9bd2abbbae26720fb2bd4b9a381dca9 Mon Sep 17 00:00:00 2001 From: garbled1 Date: Wed, 18 Nov 2020 05:59:23 -0700 Subject: [PATCH 25/28] I'm just curious if this works --- custom_components/ecowitt/binary_sensor.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/custom_components/ecowitt/binary_sensor.py b/custom_components/ecowitt/binary_sensor.py index f47dd3a..c783075 100644 --- a/custom_components/ecowitt/binary_sensor.py +++ b/custom_components/ecowitt/binary_sensor.py @@ -11,6 +11,7 @@ from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN _LOGGER = logging.getLogger(__name__) @@ -49,6 +50,13 @@ def is_on(self): return None return False + @property + def state(self): + """Return the state of the binary sensor.""" + if self.is_on is None: + return STATE_UNKNOWN + return STATE_ON if self.is_on else STATE_OFF + @property def device_class(self): """Return the device class.""" From e9164e99b2b728542db49e40475f233fa5a0ebc8 Mon Sep 17 00:00:00 2001 From: garbled1 Date: Wed, 18 Nov 2020 06:04:28 -0700 Subject: [PATCH 26/28] Get rid of debug logging --- custom_components/ecowitt/__init__.py | 12 ------------ custom_components/ecowitt/binary_sensor.py | 1 + custom_components/ecowitt/config_flow.py | 3 --- 3 files changed, 1 insertion(+), 15 deletions(-) diff --git a/custom_components/ecowitt/__init__.py b/custom_components/ecowitt/__init__.py index f53454c..6c641f7 100644 --- a/custom_components/ecowitt/__init__.py +++ b/custom_components/ecowitt/__init__.py @@ -108,7 +108,6 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up the Ecowitt component from UI.""" - _LOGGER.warning("async_setup_entry") if hass.data.get(DOMAIN) is None: hass.data.setdefault(DOMAIN, {}) @@ -123,7 +122,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) # Store config - _LOGGER.error(entry.entry_id) hass.data[DOMAIN][entry.entry_id] = {} ecowitt_data = hass.data[DOMAIN][entry.entry_id] ecowitt_data[DATA_STATION] = {} @@ -233,7 +231,6 @@ async def _first_data_rec(weather_data): # load the sensors we have for sensor in ws.last_values.keys(): - _LOGGER.warning("check sensor " + sensor) check_and_append_sensor(sensor) if (not ecowitt_data[REG_ENTITIES][TYPE_SENSOR] @@ -241,12 +238,7 @@ async def _first_data_rec(weather_data): _LOGGER.error("No sensors found to monitor, check device config.") return False - _LOGGER.warning("calling load_platform") - _LOGGER.warning(entry.data) - _LOGGER.warning(entry) - for component in ECOWITT_PLATFORMS: - _LOGGER.warning("calling fes for " + component) hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) ) @@ -256,7 +248,6 @@ async def _first_data_rec(weather_data): async def _async_ecowitt_update_cb(weather_data): """Primary update callback called from pyecowitt.""" _LOGGER.debug("Primary update callback triggered.") - _LOGGER.warning("update cb called for id=" + entry.entry_id) new_sensors = {} for component in ECOWITT_PLATFORMS: @@ -316,8 +307,6 @@ def async_add_ecowitt_entities(hass, entry, entity_type, platform, async_add_entities, discovery_info): entities = [] - _LOGGER.warning("Calling async_add_ecowitt_entities") - _LOGGER.warning(discovery_info) if discovery_info is None: return @@ -384,7 +373,6 @@ async def async_added_to_hass(self): @callback def _update_callback(self) -> None: """Call from dispatcher when state changes.""" - _LOGGER.warning("Updating state with new data. %s", self._name) self.async_schedule_update_ha_state(force_refresh=True) @property diff --git a/custom_components/ecowitt/binary_sensor.py b/custom_components/ecowitt/binary_sensor.py index c783075..ee3a6b1 100644 --- a/custom_components/ecowitt/binary_sensor.py +++ b/custom_components/ecowitt/binary_sensor.py @@ -53,6 +53,7 @@ def is_on(self): @property def state(self): """Return the state of the binary sensor.""" + # Don't claim a leak is cleared if the sensor is out of range if self.is_on is None: return STATE_UNKNOWN return STATE_ON if self.is_on else STATE_OFF diff --git a/custom_components/ecowitt/config_flow.py b/custom_components/ecowitt/config_flow.py index 25a85a0..6e2cd62 100644 --- a/custom_components/ecowitt/config_flow.py +++ b/custom_components/ecowitt/config_flow.py @@ -34,9 +34,6 @@ async def validate_input(hass: core.HomeAssistant, data): """Validate user input.""" for entry in hass.config_entries.async_entries(DOMAIN): - _LOGGER.warning(entry.data[CONF_PORT]) - _LOGGER.warning(data[CONF_PORT]) - _LOGGER.warning(data) if entry.data[CONF_PORT] == data[CONF_PORT]: raise AlreadyConfigured return {"title": f"Ecowitt on port {data[CONF_PORT]}"} From c494587ba208fea5fc6deb1f6f16305c030bbbc5 Mon Sep 17 00:00:00 2001 From: garbled1 Date: Fri, 20 Nov 2020 04:46:49 -0700 Subject: [PATCH 27/28] Update readme for 0.5, add bling --- README.md | 228 ++++++++++++++++++++++++++++++++++++----- md.images/overview.png | Bin 0 -> 35099 bytes 2 files changed, 205 insertions(+), 23 deletions(-) create mode 100644 md.images/overview.png diff --git a/README.md b/README.md index 287ecfe..b27e43a 100644 --- a/README.md +++ b/README.md @@ -3,37 +3,219 @@ # Ecowitt Weather Station integration for home-assistant Ecowitt driver for homeassistant +![Bling](https://raw.githubusercontent.com/garbled1/homeassistant_ecowitt/master/md.images/overview.png) + ## Configuration: - ecowitt: - port: 4199 - barounit: metric - windunit: imperial - rainunit: imperial - lightningunit: imperial - windchillunit: hybrid +Configuration for the Ecowitt integration is now performed via a config flow +as opposed to yaml configuration file. + +In home assistant, you will find it under integrations, search for ecowitt once +the custom component is installed. +You must select the port when enabling, see below section on "How to set up". + +There are a few options available once the integration is setup, these are +available in the "options" dialog in the integrations box for the component. + +* Barometer Unit (default metric) +* Wind Unit (default imperial) +* Rainfall Unit (default imperial) +* Lightning Unit (default imperial) +* Windchill Unit (default hybrid) + +Windchill can be one of "hybrid", "old", or "new". +Defaults for units are as shown above. +Units can be one of "metric" or "imperial". + +Note that if you change the units, it will create a new sensor for the +different unit. +For example, if you had wind set to imperial, "sensor.wind_speed" +would have been your data entity, but switching to metric will create a +"sensor.wind_speed_2". +You will see in the entities page the original "sensor.wind_speed" will be +marked with a status of "Restored". +You can safely delete the old sensor once you validate you are seeing data +on the new one. +Be sure to update any automations/etc that reference the old sensor. + + +### Breaking changes + +Version 0.5 converts this to a config flow from the previous yaml config method. +Once you restart hass, it will attempt to read your old config from yaml, and +port it over to the config flow. +Verify that it did so correctly, and double check the options match what you +expect for the units under "Options". + +Additionally in 0.5, the battery sensors have been significantly changed. +Previously all batteries were simple floats with the raw value displayed. +There are 3 types of batteries that the ecowitt displays data for: + +* Simple good/bad batteries. These are now binary sensors. This will leave + A dead entry in your entities for the old battery sensor. You may safely + delete that entity. +* Voltage readings. A few batteries display a voltage (soil, WH80). + A soil battery is normally 1.5v, so a good alarm might be around 1.3? + WH80 batteries seem to be about 2.38 - 2.4, so maybe in the 2.3 to 2.2 range + for an alarm? +* Other batteries will now show as a percentage. + The raw sensor gives a number from 0-5, this is simply multiplied by 20 + to give a percentage of 0-100. + +If you were monitoring one of these, be sure to update any automations. + +There was a bug in the wind gust sensors, where it was not being affected by +the windunit setting, once you load 0.5, you may find a dead entity for your +wind gust sensors that were setup for the wrong unit. +You may delete these. -Windchill can be one of "hybrid", "old", or "new". Defaults for units are -as shown above. The only mandatory "option" is "port". Units can be one of -"metric" or "imperial". ## How to set up: Use the WS View app (on your phone) for your Ecowitt device, and connect to it. -Pick menu -> device list -> Pick your station. -Hit next repeatedly to move to the last screen titled "Customized" - -Pick the protocol Ecowitt, and put in the ip/hostname of your hass server. -Path doesn't matter as long as it ends in /, leave the default, or change it to -just /. -Pick a port that is not in use on the server (netstat -lt). -(4199 is probably a good default) -Pick a reasonable value for updates, like 60 seconds. -Save configuration. The Ecowitt should then start attempting to send data -to your server. - -Add config as shown above to configuration.yaml. Port is required. + +1. Pick menu -> device list -> Pick your station. +1. Hit next repeatedly to move to the last screen titled "Customized" +1. Pick the protocol Ecowitt, and put in the ip/hostname of your hass server. +1. Path doesn't matter as long as it ends in /, leave the default, or change it to + just /. +1. Pick a port that is not in use on the server (netstat -lt). + (4199 is probably a good default) +1. Pick a reasonable value for updates, like 60 seconds. +1. Save configuration. + +The Ecowitt should then start attempting to send data to your server. + +In home assistant, navigate to integrations, and search for the ecowitt component. +You will need to supply a port, and an optional name for the station (if you have +multiple stations this might be useful) Pick the same port you did in the wsview app. +One note: You may wish to setup the integration, and change the options for +the various units prior to setting up the physical device. +This will prevent creation of any entities for the wrong measurement unit from +being created if you decide to change one to a non-default. + +## Errors in the logs + If you get an error in the logs about an unhandled sensor, open an issue and paste the log message so I can add the sensor. + +If you have a sensor that is barely in range, you will see a bunch of messages +in the logs about the sensor not being in the most recent update. +This can also be caused by a sensor that has a low battery. +If you know this sensor is just badly placed, you can ignore these, but if you +start seeing them for previously reliable sensors, check the batteries. + + +## Delay on startup + +Older versions of this component would cause homeassistant to freeze on startup +waiting for the first sensor burst. +This is no longer the case. +Sensors will now show up as restored until the first data packet is recieved +from the ecowitt. +There should be no delay on startup at all. + + +## A note on leak sensors + +Because leak sensors may be potentially important devices for automation, +they handle going out of range somewhat differently. +If a leak sensor is missing from the last data update from the ecowitt, it +will go into state Unknown. +If you rely upon a leak sensor for something vital, I suggest testing your +automation, by disconnecting the battery from the sensor, and validating +your code does something sane. + + +## I want a pretty card for my weather + +I highly reccomend https://github.com/r-renato/ha-card-weather-conditions +It's fairly simple to setup as a custom card, and produces lovely results. +You can easily set it up to combine local data from your sensors, with +forcast data from external sources for sensors you don't have +(like pollen counts, for example). + +This is a copy of my setup. +Sensors named with the sensor.cc_ are from the climacell external source, +other sensors are my local weatherstation. + +``` +air_quality: + co: sensor.cc_co + epa_aqi: sensor.cc_epa_aqi + epa_health_concern: sensor.cc_epa_health_concern + no2: sensor.cc_no2 + o3: sensor.cc_o3 + pm10: sensor.cc_pm10 + pm25: sensor.pm2_5_1 + so2: sensor.cc_so2 +animation: true +name: WeatherStation +pollen: + grass: + entity: sensor.cc_pollen_grass + high: 3 + low: 1 + max: 5 + min: 0 + tree: + entity: sensor.cc_pollen_tree + high: 3 + low: 1 + max: 5 + min: 0 + weed: + entity: sensor.cc_pollen_weed + high: 3 + low: 1 + max: 5 + min: 0 +type: 'custom:ha-card-weather-conditions' +weather: + current: + current_conditions: sensor.cc_weather_condition + feels_like: sensor.windchill + forecast: true + humidity: sensor.humidity + precipitation: sensor.rain_rate + pressure: sensor.absolute_pressure + sun: sun.sun + temperature: sensor.outdoor_temperature + visibility: sensor.cc_visibility + wind_bearing: sensor.wind_direction + wind_speed: sensor.wind_speed + forecast: + icons: + day_1: sensor.cc_weather_condition_0d + day_2: sensor.cc_weather_condition_1d + day_3: sensor.cc_weather_condition_2d + day_4: sensor.cc_weather_condition_3d + day_5: sensor.cc_weather_condition_4d + precipitation_intensity: + day_1: sensor.cc_max_precipitation_0d + day_2: sensor.cc_max_precipitation_1d + day_3: sensor.cc_max_precipitation_2d + day_4: sensor.cc_max_precipitation_3d + day_5: sensor.cc_max_precipitation_4d + precipitation_probability: + day_1: sensor.cc_precipitation_probability_0d + day_2: sensor.cc_precipitation_probability_1d + day_3: sensor.cc_precipitation_probability_2d + day_4: sensor.cc_precipitation_probability_3d + day_5: sensor.cc_precipitation_probability_4d + temperature_high: + day_1: sensor.cc_max_temperature_0d + day_2: sensor.cc_max_temperature_1d + day_3: sensor.cc_max_temperature_2d + day_4: sensor.cc_max_temperature_3d + day_5: sensor.cc_max_temperature_4d + temperature_low: + day_1: sensor.cc_min_temperature_0d + day_2: sensor.cc_min_temperature_1d + day_3: sensor.cc_min_temperature_2d + day_4: sensor.cc_min_temperature_3d + day_5: sensor.cc_min_temperature_4d + icons_model: climacell +``` diff --git a/md.images/overview.png b/md.images/overview.png new file mode 100644 index 0000000000000000000000000000000000000000..d773e7d065c4b620adaa0781b644b4d7c5d89cb0 GIT binary patch literal 35099 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 ke=XoqpZ{l9`uQ%&xp{V0eyh#YJ@DV Date: Fri, 20 Nov 2020 04:53:18 -0700 Subject: [PATCH 28/28] One more note --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index b27e43a..2be61ba 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,9 @@ the windunit setting, once you load 0.5, you may find a dead entity for your wind gust sensors that were setup for the wrong unit. You may delete these. +Once your configuration has been moved, you should delete the old ecowitt +section from your configuration.yaml file and restart hass. + ## How to set up: