def process_data_payload(data: dict) -> dict: """Process incoming data from an Ecowitt device.""" for ignore_key in ("dateutc", "freq", "model", "stationtype"): data.pop(ignore_key, None) humidity = int(data[DATA_POINT_HUMIDITY]) temperature = meteocalc.Temp(data[DATA_POINT_TEMPF], "f") wind_speed = float(data[DATA_POINT_WINDSPEEDMPH]) dew_point = meteocalc.dew_point(temperature, humidity) data[DATA_POINT_DEWPOINT] = dew_point.f heat_index = meteocalc.heat_index(temperature, humidity) data[DATA_POINT_HEATINDEX] = heat_index.f try: wind_chill = meteocalc.wind_chill(temperature, wind_speed) except ValueError as err: LOGGER.debug( "%s (temperature: %s, wind speed: %s)", err, temperature.f, wind_speed, ) else: data[DATA_POINT_WINDCHILL] = wind_chill.f feels_like = meteocalc.feels_like(temperature, humidity, wind_speed) data[DATA_POINT_FEELSLIKEF] = feels_like.f return data
def get_entity_description(key: str, value: Union[float, int, str]) -> EntityDescription: """Get an entity description for a data key. 1. Return a specific data point if it exists. 2. Return a globbed data point if it exists. 3. Return defaults if no specific or globbed data points exist. """ if DATA_POINT_GLOB_BATT in key and isinstance(value, str): # Because Ecowitt doesn't give us a clear way to know what sort of battery # we're looking at (a binary on/off battery or one that reports voltage), we # check its value: strings are binary, floats are voltage: return ENTITY_DESCRIPTIONS[DATA_POINT_GLOB_BATT_BINARY] if key in ENTITY_DESCRIPTIONS: return ENTITY_DESCRIPTIONS[key] globbed_descriptions = [ v for k, v in ENTITY_DESCRIPTIONS.items() if k in key ] if globbed_descriptions: return globbed_descriptions[0] LOGGER.info("No entity description found for key: %s", key) return EntityDescription(platform=PLATFORM_SENSOR)
def main(): """Run.""" args = get_arguments() logging.basicConfig(level=getattr(logging, args.log_level)) LOGGER.debug("Using arguments: %s", args) app = web.Application() app.add_routes([web.post(args.endpoint, async_respond_to_ecowitt_data)]) app["args"] = args app["hass_discovery_managers"] = {} mqtt = app["mqtt"] = MQTT( args.mqtt_broker, port=args.mqtt_port, username=args.mqtt_username, password=args.mqtt_password, ) async def connect_mqtt(_) -> None: """Connect the MQTT broker.""" await mqtt.async_connect() async def disconnect_mqtt(_) -> None: """Disconnect the MQTT broker.""" await mqtt.async_disconnect() app.on_startup.append(connect_mqtt) app.on_cleanup.append(disconnect_mqtt) web.run_app(app, port=args.port)
async def async_disconnect(self) -> None: """Disconnect from the MQTT broker.""" try: await self._client.disconnect() except MqttError as err: LOGGER.error("Error while disconnecting from MQTT broker: %s", err) LOGGER.debug("Disconnected from MQTT broker")
async def async_connect(self) -> None: """Connect to the MQTT broker.""" try: await self._client.connect() except MqttError as err: LOGGER.error("Error while connecting to MQTT broker: %s", err) LOGGER.debug("Connected to MQTT broker")
def main(): """Run.""" args = get_arguments() logging.basicConfig(level=getattr(logging, args.log_level)) LOGGER.debug("Using arguments: %s", args) app = web.Application() app.add_routes([web.post(args.endpoint, async_publish_payload)]) app["args"] = args app["hass_discovery_managers"] = {} web.run_app(app, port=args.port)
async def async_publish(self, topic: str, data: Union[dict, float, str]) -> None: """Publish data to an MQTT topic.""" if isinstance(data, dict): payload = json.dumps(data).encode("utf-8") elif isinstance(data, str): payload = data.encode("utf-8") else: payload = str(data).encode("utf-8") try: await self._client.publish(topic, payload) LOGGER.info("Data published to topic %s: %s", topic, data) except MqttError as err: LOGGER.error("Error while publishing data to MQTT: %s", err)
async def async_publish_payload(request: web.Request): """Define the endpoint for the Ecowitt device to post data to.""" args = request.app["args"] payload = dict(await request.post()) LOGGER.debug("Received data from Ecowitt device: %s", payload) data_processor = DataProcessor(payload, args.unit_system) data = data_processor.generate_data() client = Client( args.mqtt_broker, port=args.mqtt_port, username=args.mqtt_username, password=args.mqtt_password, logger=LOGGER, ) if args.hass_discovery: discovery_managers = request.app["hass_discovery_managers"] try: discovery_manager = discovery_managers[data_processor.unique_id] except KeyError: if args.hass_discovery_prefix: discovery_manager = discovery_managers[ data_processor.unique_id] = HassDiscovery( data_processor.unique_id, args.unit_system, discovery_prefix=args.hass_discovery_prefix, ) else: discovery_manager = discovery_managers[ data_processor.unique_id] = HassDiscovery( data_processor.unique_id, args.unit_system) await _async_publish_to_hass_discovery(client, data, discovery_manager) else: if args.mqtt_topic: topic = args.mqtt_topic else: topic = f"ecowitt2mqtt/{data_processor.unique_id}" await _async_publish_to_topic(client, data, topic=topic)
def calculate_wind_chill( temperature: float, wind_speed: float, *, input_unit_system: str = UNIT_SYSTEM_IMPERIAL, output_unit_system: str = UNIT_SYSTEM_IMPERIAL) -> Optional[float]: """Calculate wind chill in the appropriate unit system.""" temp_obj = _get_temperature_object(temperature, input_unit_system) try: wind_chill_obj = meteocalc.wind_chill(temp_obj, wind_speed) except ValueError as err: LOGGER.debug("%s (temperature: %s, wind speed: %s)", err, temp_obj, wind_speed) return None if output_unit_system == UNIT_SYSTEM_IMPERIAL: value = round(wind_chill_obj.f, 1) else: value = round(wind_chill_obj.c, 1) return cast(float, value)
async def _async_publish_to_hass_discovery( client: Client, data: dict, discovery_manager: HassDiscovery) -> None: """Publish data to appropriate topics for Home Assistant Discovery.""" LOGGER.debug( "Publishing according to Home Assistant MQTT Discovery standard") try: async with client: tasks = [] for key, value in data.items(): config_payload = discovery_manager.get_config_payload(key) config_topic = discovery_manager.get_config_topic(key) tasks.append( client.publish(config_topic, _generate_payload(config_payload))) tasks.append( client.publish( config_payload["availability_topic"], _generate_payload("online"), )) tasks.append( client.publish(config_payload["state_topic"], _generate_payload(value))) await asyncio.gather(*tasks) except MqttError as err: LOGGER.error("Error while publishing to HASS Discovery: %s", err) return LOGGER.info("Published to HASS discovery: %s", data)
def get_device_from_raw_payload(payload: Dict[str, Any]) -> Device: """Return a device based upon a model string.""" model = payload["model"] station_type = payload.get("stationtype", DEFAULT_STATION_TYPE) unique_id = payload.get("PASSKEY", DEFAULT_UNIQUE_ID) if model in DEVICE_DATA: manufacturer, name = DEVICE_DATA[model] else: matches = [v for k, v in DEVICE_DATA.items() if k in model] if matches: manufacturer, name = matches[0] else: LOGGER.info( ("Unknown device; please report it at " "https://github.com/bachya/ecowitt2mqtt (payload: %s)"), payload, ) manufacturer = DEFAULT_MANUFACTURER name = DEFAULT_NAME return Device(unique_id, manufacturer, name, station_type)
async def async_publish_payload(request: web.Request) -> None: """Define the endpoint for the Ecowitt device to post data to.""" args = request.app["args"] payload = dict(await request.post()) LOGGER.debug("Received data from Ecowitt device: %s", payload) data_processor = DataProcessor(payload, args) data = data_processor.generate_data() client = Client( args.mqtt_broker, port=args.mqtt_port, username=args.mqtt_username, password=args.mqtt_password, logger=LOGGER, max_concurrent_outgoing_calls=DEFAULT_MAX_MQTT_CALLS, ) if args.hass_discovery: discovery_managers = request.app["hass_discovery_managers"] if data_processor.device.unique_id in discovery_managers: discovery_manager = discovery_managers[data_processor.device.unique_id] else: discovery_manager = HassDiscovery(data_processor.device, args) discovery_managers[data_processor.device.unique_id] = discovery_manager await _async_publish_to_hass_discovery(client, data, discovery_manager) else: if args.mqtt_topic: topic = args.mqtt_topic else: topic = f"ecowitt2mqtt/{data_processor.device.unique_id}" await _async_publish_to_topic(client, data, topic)
async def _async_publish_to_topic( client: Client, data: Dict[str, Any], topic: str ) -> None: """Publish data to a single MQTT topic.""" LOGGER.debug("Publishing entire device payload to single topic: %s", topic) try: async with client: await client.publish(topic, _generate_payload(data)) except MqttError as err: LOGGER.error("Error while publishing to %s: %s", topic, err) return LOGGER.info("Published to %s: %s", topic, data)
async def async_respond_to_ecowitt_data(request: web.Request): """Define the endpoint for the Ecowitt device to post data to.""" args = request.app["args"] hass_discovery_managers = request.app["hass_discovery_managers"] mqtt = request.app["mqtt"] data = dict(await request.post()) LOGGER.debug("Received data from Ecowitt device: %s", data) data = process_data_payload(data) unique_id = data.pop("PASSKEY") if not request.app["args"].hass_discovery: LOGGER.debug("Publishing entire device payload to single topic") if request.app["args"].mqtt_topic: topic = request.app["args"].mqtt_topic else: topic = f"ecowitt2mqtt/{unique_id}" return await mqtt.async_publish(topic, data) LOGGER.debug( "Publishing according to Home Assistant MQTT Discovery standard") try: discovery = hass_discovery_managers[unique_id] except KeyError: if args.hass_discovery_prefix: discovery = hass_discovery_managers[unique_id] = HassDiscovery( unique_id, discovery_prefix=args.hass_discovery_prefix) else: discovery = hass_discovery_managers[unique_id] = HassDiscovery( unique_id) tasks = [] for key, value in data.items(): config_payload = discovery.get_config_payload(key) config_topic = discovery.get_config_topic(key) tasks.append(mqtt.async_publish(config_topic, config_payload)) tasks.append( mqtt.async_publish(config_payload["availability_topic"], "online")) tasks.append(mqtt.async_publish(config_payload["state_topic"], value)) await asyncio.gather(*tasks)
def __post_init__(self): """Set up some additional attributes from passed-in data.""" object.__setattr__(self, "unique_id", self._input.pop("PASSKEY", DEFAULT_UNIQUE_ID)) # Only store data keys that we explicitly care about: object.__setattr__( self, "_data", {k: v for k, v in self._input.items() if k not in KEYS_TO_IGNORE}, ) # Determine properties necessary for the calculated properties: if DATA_POINT_TEMPF in self._data: object.__setattr__( self, "_temperature_obj", meteocalc.Temp(self._data[DATA_POINT_TEMPF], "f"), ) if DATA_POINT_HUMIDITY in self._data: object.__setattr__( self, "_humidity", round(float(self._data[DATA_POINT_HUMIDITY]), 1)) if DATA_POINT_WINDSPEEDMPH in self._data: object.__setattr__( self, "_wind_speed", round(float(self._data[DATA_POINT_WINDSPEEDMPH]))) # Determine calculated properties: if self._temperature_obj and self._humidity: object.__setattr__( self, "_dew_point_obj", meteocalc.dew_point(self._temperature_obj, self._humidity), ) object.__setattr__( self, "_heat_index_obj", meteocalc.heat_index(self._temperature_obj, self._humidity), ) if self._temperature_obj and self._wind_speed: if self._humidity: object.__setattr__( self, "_feels_like_obj", meteocalc.feels_like(self._temperature_obj, self._humidity, self._wind_speed), ) try: object.__setattr__( self, "_wind_chill_obj", meteocalc.wind_chill(self._temperature_obj, self._wind_speed), ) except ValueError as err: LOGGER.debug( "%s (temperature: %s, wind speed: %s)", err, self._temperature_obj, self._wind_speed, )