Home Assistant Git Exporter
This commit is contained in:
330
config/custom_components/blitzortung/__init__.py
Normal file
330
config/custom_components/blitzortung/__init__.py
Normal file
@@ -0,0 +1,330 @@
|
||||
"""The blitzortung integration."""
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import time
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, UnitOfLength
|
||||
from homeassistant.core import callback, HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
from homeassistant.util.unit_system import IMPERIAL_SYSTEM
|
||||
from homeassistant.util.unit_conversion import DistanceConverter
|
||||
from . import const
|
||||
from .const import (
|
||||
CONF_IDLE_RESET_TIMEOUT,
|
||||
CONF_MAX_TRACKED_LIGHTNINGS,
|
||||
CONF_RADIUS,
|
||||
CONF_TIME_WINDOW,
|
||||
DEFAULT_IDLE_RESET_TIMEOUT,
|
||||
DEFAULT_MAX_TRACKED_LIGHTNINGS,
|
||||
DEFAULT_RADIUS,
|
||||
DEFAULT_TIME_WINDOW,
|
||||
DEFAULT_UPDATE_INTERVAL,
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
)
|
||||
from .geohash_utils import geohash_overlap
|
||||
from .mqtt import MQTT, MQTT_CONNECTED, MQTT_DISCONNECTED
|
||||
from .version import __version__
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{DOMAIN: vol.Schema({vol.Optional(const.SERVER_STATS, default=False): bool})},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict):
|
||||
"""Initialize basic config of blitzortung component."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN]["config"] = config.get(DOMAIN) or {}
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
"""Set up blitzortung from a config entry."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
config = hass.data[DOMAIN].get("config") or {}
|
||||
|
||||
latitude = config_entry.options.get(CONF_LATITUDE, hass.config.latitude)
|
||||
longitude = config_entry.options.get(CONF_LONGITUDE, hass.config.longitude)
|
||||
radius = config_entry.options.get(CONF_RADIUS, DEFAULT_RADIUS)
|
||||
max_tracked_lightnings = config_entry.options.get(
|
||||
CONF_MAX_TRACKED_LIGHTNINGS, DEFAULT_MAX_TRACKED_LIGHTNINGS
|
||||
)
|
||||
time_window_seconds = (
|
||||
config_entry.options.get(CONF_TIME_WINDOW, DEFAULT_TIME_WINDOW) * 60
|
||||
)
|
||||
if max_tracked_lightnings >= 500:
|
||||
_LOGGER.warning(
|
||||
"Large number of tracked lightnings: %s, it may lead to"
|
||||
"bigger memory usage / unstable frontend",
|
||||
max_tracked_lightnings,
|
||||
)
|
||||
|
||||
if hass.config.units == IMPERIAL_SYSTEM:
|
||||
radius_mi = radius
|
||||
radius = DistanceConverter.convert(radius, UnitOfLength.MILES, UnitOfLength.KILOMETERS)
|
||||
_LOGGER.info("imperial system, %s mi -> %s km", radius_mi, radius)
|
||||
|
||||
coordinator = BlitzortungCoordinator(
|
||||
hass,
|
||||
latitude,
|
||||
longitude,
|
||||
radius,
|
||||
max_tracked_lightnings,
|
||||
time_window_seconds,
|
||||
DEFAULT_UPDATE_INTERVAL,
|
||||
server_stats=config.get(const.SERVER_STATS),
|
||||
)
|
||||
|
||||
hass.data[DOMAIN][config_entry.entry_id] = coordinator
|
||||
|
||||
async def start_platforms():
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_setup(config_entry, component)
|
||||
for component in PLATFORMS
|
||||
]
|
||||
)
|
||||
await coordinator.connect()
|
||||
|
||||
hass.async_create_task(start_platforms())
|
||||
|
||||
if not config_entry.update_listeners:
|
||||
config_entry.add_update_listener(async_update_options)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_update_options(hass, config_entry):
|
||||
"""Update options."""
|
||||
_LOGGER.info("async_update_options")
|
||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
"""Unload a config entry."""
|
||||
coordinator = hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
await coordinator.disconnect()
|
||||
_LOGGER.info("disconnected")
|
||||
|
||||
# cleanup platforms
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(config_entry, component)
|
||||
for component in PLATFORMS
|
||||
]
|
||||
)
|
||||
)
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def async_migrate_entry(hass, entry):
|
||||
_LOGGER.debug("Migrating Blitzortung entry from Version %s", entry.version)
|
||||
if entry.version == 1:
|
||||
latitude = entry.data[CONF_LATITUDE]
|
||||
longitude = entry.data[CONF_LONGITUDE]
|
||||
radius = entry.data[CONF_RADIUS]
|
||||
name = entry.data[CONF_NAME]
|
||||
|
||||
entry.unique_id = f"{latitude}-{longitude}-{name}-lightning"
|
||||
entry.data = {CONF_NAME: name}
|
||||
entry.options = {
|
||||
CONF_LATITUDE: latitude,
|
||||
CONF_LONGITUDE: longitude,
|
||||
CONF_RADIUS: radius,
|
||||
}
|
||||
entry.version = 2
|
||||
if entry.version == 2:
|
||||
entry.options = dict(entry.options)
|
||||
entry.options[CONF_IDLE_RESET_TIMEOUT] = DEFAULT_IDLE_RESET_TIMEOUT
|
||||
entry.version = 3
|
||||
if entry.version == 3:
|
||||
entry.options = dict(entry.options)
|
||||
entry.options[CONF_TIME_WINDOW] = entry.options.pop(
|
||||
CONF_IDLE_RESET_TIMEOUT, DEFAULT_TIME_WINDOW
|
||||
)
|
||||
entry.version = 4
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class BlitzortungCoordinator:
|
||||
def __init__(
|
||||
self,
|
||||
hass,
|
||||
latitude,
|
||||
longitude,
|
||||
radius, # unit: km
|
||||
max_tracked_lightnings,
|
||||
time_window_seconds,
|
||||
update_interval,
|
||||
server_stats=False,
|
||||
):
|
||||
"""Initialize."""
|
||||
self.hass = hass
|
||||
self.latitude = latitude
|
||||
self.longitude = longitude
|
||||
self.radius = radius
|
||||
self.max_tracked_lightnings = max_tracked_lightnings
|
||||
self.time_window_seconds = time_window_seconds
|
||||
self.server_stats = server_stats
|
||||
self.last_time = 0
|
||||
self.sensors = []
|
||||
self.callbacks = []
|
||||
self.lightning_callbacks = []
|
||||
self.on_tick_callbacks = []
|
||||
self.geohash_overlap = geohash_overlap(
|
||||
self.latitude, self.longitude, self.radius
|
||||
)
|
||||
self._disconnect_callbacks = []
|
||||
self.unloading = False
|
||||
|
||||
_LOGGER.info(
|
||||
"lat: %s, lon: %s, radius: %skm, geohashes: %s",
|
||||
self.latitude,
|
||||
self.longitude,
|
||||
self.radius,
|
||||
self.geohash_overlap,
|
||||
)
|
||||
|
||||
self.mqtt_client = MQTT(
|
||||
hass,
|
||||
"blitzortung.ha.sed.pl",
|
||||
1883,
|
||||
)
|
||||
|
||||
self._disconnect_callbacks.append(
|
||||
async_dispatcher_connect(
|
||||
self.hass, MQTT_CONNECTED, self._on_connection_change
|
||||
)
|
||||
)
|
||||
self._disconnect_callbacks.append(
|
||||
async_dispatcher_connect(
|
||||
self.hass, MQTT_DISCONNECTED, self._on_connection_change
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _on_connection_change(self, *args, **kwargs):
|
||||
if self.unloading:
|
||||
return
|
||||
for sensor in self.sensors:
|
||||
sensor.async_write_ha_state()
|
||||
|
||||
def compute_polar_coords(self, lightning):
|
||||
dy = (lightning["lat"] - self.latitude) * math.pi / 180
|
||||
dx = (
|
||||
(lightning["lon"] - self.longitude)
|
||||
* math.pi
|
||||
/ 180
|
||||
* math.cos(self.latitude * math.pi / 180)
|
||||
)
|
||||
distance = round(math.sqrt(dx * dx + dy * dy) * 6371, 1)
|
||||
azimuth = round(math.atan2(dx, dy) * 180 / math.pi) % 360
|
||||
|
||||
lightning[SensorDeviceClass.DISTANCE] = distance
|
||||
lightning[const.ATTR_LIGHTNING_AZIMUTH] = azimuth
|
||||
|
||||
async def connect(self):
|
||||
await self.mqtt_client.async_connect()
|
||||
_LOGGER.info("Connected to Blitzortung proxy mqtt server")
|
||||
for geohash_code in self.geohash_overlap:
|
||||
geohash_part = "/".join(geohash_code)
|
||||
await self.mqtt_client.async_subscribe(
|
||||
"blitzortung/1.1/{}/#".format(geohash_part), self.on_mqtt_message, qos=0
|
||||
)
|
||||
if self.server_stats:
|
||||
await self.mqtt_client.async_subscribe(
|
||||
"$SYS/broker/#", self.on_mqtt_message, qos=0
|
||||
)
|
||||
await self.mqtt_client.async_subscribe(
|
||||
"component/hello", self.on_hello_message, qos=0
|
||||
)
|
||||
|
||||
self._disconnect_callbacks.append(
|
||||
async_track_time_interval(
|
||||
self.hass, self._tick, const.DEFAULT_UPDATE_INTERVAL
|
||||
)
|
||||
)
|
||||
|
||||
async def disconnect(self):
|
||||
self.unloading = True
|
||||
await self.mqtt_client.async_disconnect()
|
||||
for cb in self._disconnect_callbacks:
|
||||
cb()
|
||||
|
||||
def on_hello_message(self, message, *args):
|
||||
def parse_version(version_str):
|
||||
return tuple(map(int, version_str.split(".")))
|
||||
|
||||
data = json.loads(message.payload)
|
||||
latest_version_str = data.get("latest_version")
|
||||
if latest_version_str:
|
||||
default_message = (
|
||||
f"New version {latest_version_str} is available. "
|
||||
f"[Check it out](https://github.com/mrk-its/homeassistant-blitzortung)"
|
||||
)
|
||||
latest_version_message = data.get("latest_version_message", default_message)
|
||||
latest_version_title = data.get("latest_version_title", "Blitzortung")
|
||||
latest_version = parse_version(latest_version_str)
|
||||
current_version = parse_version(__version__)
|
||||
if latest_version > current_version:
|
||||
_LOGGER.info("new version is available: %s", latest_version_str)
|
||||
self.hass.components.persistent_notification.async_create(
|
||||
title=latest_version_title,
|
||||
message=latest_version_message,
|
||||
notification_id="blitzortung_new_version_available",
|
||||
)
|
||||
|
||||
async def on_mqtt_message(self, message, *args):
|
||||
for callback in self.callbacks:
|
||||
callback(message)
|
||||
if message.topic.startswith("blitzortung/1.1"):
|
||||
lightning = json.loads(message.payload)
|
||||
self.compute_polar_coords(lightning)
|
||||
if lightning[SensorDeviceClass.DISTANCE] < self.radius:
|
||||
_LOGGER.debug("lightning data: %s", lightning)
|
||||
self.last_time = time.time()
|
||||
for callback in self.lightning_callbacks:
|
||||
await callback(lightning)
|
||||
for sensor in self.sensors:
|
||||
sensor.update_lightning(lightning)
|
||||
|
||||
def register_sensor(self, sensor):
|
||||
self.sensors.append(sensor)
|
||||
self.register_on_tick(sensor.tick)
|
||||
|
||||
def register_message_receiver(self, message_cb):
|
||||
self.callbacks.append(message_cb)
|
||||
|
||||
def register_lightning_receiver(self, lightning_cb):
|
||||
self.lightning_callbacks.append(lightning_cb)
|
||||
|
||||
def register_on_tick(self, on_tick_cb):
|
||||
self.on_tick_callbacks.append(on_tick_cb)
|
||||
|
||||
@property
|
||||
def is_inactive(self):
|
||||
return bool(
|
||||
self.time_window_seconds
|
||||
and (time.time() - self.last_time) >= self.time_window_seconds
|
||||
)
|
||||
|
||||
@property
|
||||
def is_connected(self):
|
||||
return self.mqtt_client.connected
|
||||
|
||||
async def _tick(self, *args):
|
||||
for cb in self.on_tick_callbacks:
|
||||
cb()
|
||||
Reference in New Issue
Block a user