Files
MSC-weather-HA/weather_utility.py
2026-03-27 20:03:07 -04:00

171 lines
5.1 KiB
Python

import xml.etree.ElementTree as ET
import json
import logging
import paho.mqtt.client as mqtt
import re
import io
import os
logger = logging.getLogger(__name__)
# --- Configuration ---
BROKER = os.getenv("MQTT_BROKER", "192.168.2.17")
PORT = int(os.getenv("MQTT_PORT", 1883))
DISCOVERY_PREFIX = "homeassistant"
BASE_TOPIC = "weather/station"
def slugify(text):
return re.sub(r'\W+', '_', text).lower() if text else "unknown_station"
def parse_weather_xml(source) -> dict | None:
try:
tree = ET.parse(io.BytesIO(source))
root = tree.getroot()
current = root.find('currentConditions')
if current is None: return None
wind_speed_str = current.findtext('wind/speed')
if wind_speed_str == "calm":
wind_speed_str = "0"
# Extract and cast data
return {
"station_name": current.findtext('station'),
"condition": current.findtext('condition'),
"temperature": float(current.findtext('temperature') or 0),
"relative_humidity": float(current.findtext('relativeHumidity') or 0),
"pressure": float(current.findtext('pressure') or 0),
"wind_speed": float(wind_speed_str or 0),
"wind_bearing": float(current.findtext('wind/bearing') or 0),
}
except Exception as e:
logger.error(f"XML Parse Error: {e}")
return None
def setup_ha_discovery(client, data):
station_id = slugify(data['station_name'])
state_topic = f"{BASE_TOPIC}/{station_id}/state"
# --- SENSOR CONFIGURATION ---
# device_class: Describes what the sensor measures (temperature, humidity, etc.)
# state_class: 'measurement' allows HA to create long-term statistics/graphs
sensors = [
{
"id": "cond",
"name": "Condition",
"class": None, # Generic text sensor
"state_class": None, # Text cannot have a state class
"unit": None,
"key": "condition",
"icon": "mdi:weather-partly-cloudy" # Default icon
},
{
"id": "temp",
"name": "Temperature",
"class": "temperature",
"state_class": "measurement",
"unit": "°C",
"key": "temperature"
},
{
"id": "hum",
"name": "Humidity",
"class": "humidity",
"state_class": "measurement",
"unit": "%",
"key": "relative_humidity"
},
{
"id": "pres",
"name": "Pressure",
"class": "atmospheric_pressure",
"state_class": "measurement",
"unit": "kPa",
"key": "pressure"
},
{
"id": "wind_s",
"name": "Wind Speed",
"class": "wind_speed",
"state_class": "measurement",
"unit": "km/h",
"key": "wind_speed"
},
{
"id": "wind_b",
"name": "Wind Bearing",
"class": "wind_direction", # Correct HA class for bearing
"state_class": "measurement",
"unit": "°",
"key": "wind_bearing"
},
]
device_info = {
"identifiers": [f"weather_station_{station_id}"],
"name": data['station_name'],
"model": "XML Weather Station",
"manufacturer": "Python Script"
}
for s in sensors:
discovery_topic = f"{DISCOVERY_PREFIX}/sensor/{station_id}/{s['id']}/config"
payload = {
"name": f"{data['station_name']} {s['name']}",
"unique_id": f"{station_id}_{s['id']}",
"state_topic": state_topic,
"value_template": f"{{{{ value_json.{s['key']} }}}}",
"device": device_info
}
# Apply Optional Configurations
if s.get('class'):
payload["device_class"] = s['class']
if s.get('state_class'):
payload["state_class"] = s['state_class']
if s.get('unit'):
payload["unit_of_measurement"] = s['unit']
if s.get('icon'):
payload["icon"] = s['icon']
client.publish(discovery_topic, json.dumps(payload), retain=True)
return state_topic
def publish_weather(data):
if not data: return
client = mqtt.Client()
try:
client.connect(BROKER, PORT, 60)
# Start the network loop to handle message handshakes
client.loop_start()
# This creates the entities in HA (Retained)
state_topic = setup_ha_discovery(client, data)
# FIX 1: Add retain=True so HA sees the value even if it subscribes late
# FIX 2: Capture the message info object to verify delivery
msg_info = client.publish(state_topic, json.dumps(data), retain=True)
# FIX 3: Block execution until the message is actually sent to the broker
msg_info.wait_for_publish()
logger.info(f"Published all fields for {data['station_name']} to {state_topic}")
except Exception as e:
logger.error(f"MQTT Error: {e}")
finally:
# Stop the loop and disconnect cleanly
client.loop_stop()
client.disconnect()