Files
DevOps/Kodi/Lenovo/addons/weather.openmeteo/lib/weather.py

642 lines
17 KiB
Python

import os
import xbmc
from pathlib import Path
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor
from . import config
from . import utils
from . import conv
class Main():
### MAIN
def __init__(self, locid, mode='kodi'):
if utils.monitor.abortRequested():
return
# Import API only when needed
if mode == 'download' or mode == 'geoip' or locid.startswith('loc'):
global api
from . import api
# GeoIP
if mode == 'geoip':
api.getloc(locid)
return
# Search
if locid.startswith('loc'):
api.setloc(locid[3])
return
# Init
self.init(locid, mode)
if not config.loc.lat or not config.loc.lon:
utils.log(f'[LOC{locid}] Not configured', 1)
return
# Download
if self.mode == 'download':
# Weather
if utils.lastupdate(f'loc{locid}data') >= 3600:
with ThreadPoolExecutor(3) as pool:
pool.map(self.getdata, config.map)
if api.network():
utils.setupdate(f'loc{locid}data')
# Map
if utils.lastupdate(f'loc{locid}map') >= 604800:
self.getmap('osm')
if api.network():
utils.setupdate(f'loc{locid}map')
# Rv
if utils.lastupdate(f'loc{locid}rv') >= 3600:
with ThreadPoolExecutor(2) as pool:
pool.map(self.getmap, config.map_rv)
if api.network():
utils.setupdate(f'loc{locid}rv')
# Gc
if utils.lastupdate(f'loc{locid}gc') >= 10800:
with ThreadPoolExecutor(2) as pool:
pool.map(self.getmap, config.map_gc)
if api.network():
utils.setupdate(f'loc{locid}gc')
# Update
elif self.mode == 'update' or self.mode == 'kodi':
# Wait for service thread
if self.mode == 'kodi':
utils.monitor.waitForService()
# KODI
if not config.addon.full:
for map in config.map:
self.setdata(map)
self.setother()
utils.setprops()
# SKIN
if config.addon.skin:
config.addon.api = True
config.loc.prop = {}
for map in config.map:
self.setdata(map)
self.setother()
utils.setprops()
# Update locs
elif self.mode == 'updatelocs':
# KODI
self.setlocs()
utils.setprops()
# SKIN
if config.addon.skin:
config.addon.api = True
config.loc.prop = {}
self.setlocs()
utils.setprops()
# Notification (Queue)
elif self.mode == 'msgqueue':
for map in config.map:
self.msgqueue(map)
# Notification (Send)
elif self.mode == 'msgsend':
self.notification()
### INIT
def init(self, locid, mode):
if mode == 'download':
utils.log(f'[LOC{locid}] Initialising: mode={mode}, neterr={config.neterr}, net={api.network()}, dnscache={len(config.dnscache)}', 3)
else:
utils.log(f'[LOC{locid}] Initialising: mode={mode}', 3)
# Location
config.loc(locid)
# Vars
self.mode = mode
self.data = {}
self.today = utils.dt('nowloc').strftime('%Y-%m-%d')
# Directory
p = Path(f'{config.addon_cache}/{locid}')
p.mkdir(parents=True, exist_ok=True)
### GET DATA
def getdata(self, type):
utils.log(f'[LOC{config.loc.id}] Downloading data: {type}', 3)
api.getdata(type, config.loc.id, [ config.loc.lat, config.loc.lon, self.today ])
### SET DATA
def setdata(self, type):
# Data
self.data[type] = utils.getfile(f'{config.loc.id}/{type}.json')
if not self.data[type]:
utils.log(f'No {type} data for location {config.loc.id}', 2)
return
# Index
indexnow = utils.index("now", self.data[type])
indexmid = utils.index("mid", self.data[type])
indexday = utils.index("day", self.data[type])
# Update data
utils.log(f'[LOC{config.loc.id}] Updating data: {type}', 3)
for map in config.map.get(type):
# Current (Compatibility)
if map[0] == 'current':
self.setmap(type, map)
# Current (Advanced)
elif map[0] == 'currentskin' and config.addon.api:
self.setmap(type, map)
# Current (KODI)
elif map[0] == 'currentkodi' and self.mode == 'kodi':
self.setmap(type, map)
# Hourly (Compatibility)
elif map[0] == 'hourly':
self.setmulti(type, [ map, 'hourly', indexnow, config.maxhours, config.minhours, 'hourly' ])
if config.addon.enablehour:
self.setmulti(type, [ map, 'hourly', indexmid, config.maxhours, config.minhours, 'hour' ])
# Hourly (Advanced)
elif map[0] == 'hourlyskin' and config.addon.api:
self.setmulti(type, [ map, 'hourly', indexnow, config.maxhours, config.minhours, 'hourly' ])
if config.addon.enablehour:
self.setmulti(type, [ map, 'hourly', indexmid, config.maxhours, config.minhours, 'hour' ])
# Daily (Compatibility)
elif map[0] == 'daily':
self.setmulti(type, [ map, 'daily', indexday, config.maxdays, config.mindays, 'daily' ])
if not config.addon.api:
self.setmulti(type, [ map, 'daily', indexday, config.maxdays, config.mindays, 'day' ])
# Daily (Advanced)
elif map[0] == 'dailyskin' and config.addon.api:
self.setmulti(type, [ map, 'daily', indexday, config.maxdays, config.mindays, 'daily' ])
# Daily (KODI)
elif map[0] == 'dailykodi' and self.mode == 'kodi':
self.setmulti(type, [ map, 'daily', indexday, config.maxdays, config.mindays, 'daily' ])
self.setmulti(type, [ map, 'daily', indexday, config.maxdays, config.mindays, 'day' ])
# TimeOfDay
elif map[0] == 'timeofday':
self.setmap(type, map)
# Graph
elif map[0] == 'graph':
self.setmap(type, map)
# Alert
if map[3] == 'graph':
self.setalert(type, [ map, indexnow ])
### SET CURRENT
def setcurrent(self, type, locid):
# Data
self.data[type] = utils.getfile(f'{locid}/{type}.json')
if not self.data[type]:
utils.log(f'No {type} data for location {locid}', 2)
return
# Update data
utils.log(f'[LOC{locid}] Updating current: {type}', 3)
for map in config.map.get(type):
# Current (Compatibility)
if map[0] == 'current':
self.setmap(type, map, locid=locid)
# Current (Advanced)
elif map[0] == 'currentskin' and config.addon.api:
self.setmap(type, map, locid=locid)
### SET LOCATIONS
def setlocs(self):
locs = 0
for locid in range(1, config.addon.maxlocs):
loc = utils.setting(f'loc{locid}')
locuser = utils.setting(f'loc{locid}user')
if loc:
locs += 1
# Set "Current.X" only if called from service
if self.mode != 'kodi':
for map in config.map:
self.setcurrent(map, locid)
if locuser:
utils.addprop(f'location{locid}', locuser)
else:
utils.addprop(f'location{locid}', loc)
else:
utils.addprop(f'location{locid}', '')
utils.addprop('locations', locs)
## SET ALERT
def setalert(self, src, map):
winprops = [ 'name', 'value', 'icon', 'unit', 'time', 'hours', 'status' ]
data = self.data[src]
type = map[0][2][1]
idx = map[1]
prop = config.alert.map[type]['type']
unit = config.alert.map[type]['unit']
icon = config.alert.map[type]['icon']
name = utils.locaddon(config.alert.map[type]['loc'])
hours = 0
code = 0
value = 0
# Invalid index
if not idx:
utils.log('Index invalid, weather data is not up to date ...', 3)
return
# Alert disabled
if not utils.setting(f'alert_{prop}_enabled', 'bool', True):
utils.log(f'Disabled alert: {prop}', 3)
utils.addprop(f'alert.{prop}', '')
for winprop in winprops:
utils.addprop(f'alert.{prop}.{winprop}', '')
return
# Checking alert
utils.log(f'Checking alert: {prop}', 3)
l = []
ll = []
for index in range(idx, idx + config.addon.alerthours):
try:
v = int(data[map[0][1][0]][map[0][1][1]][index])
vv = int(data[map[0][1][0]]['time'][index])
except:
if not self.mode == 'msgqueue':
utils.addprop(f'alert.{prop}', 0)
for winprop in winprops:
utils.addprop(f'alert.{prop}.{winprop}', '')
return
else:
l.append(v)
ll.append(vv)
for c, d in [(x, y) for x in [ 3, 2, 1 ] for y in [ 'high', 'low', 'wmo' ] ]:
alert = f'alert_{prop}_{d}_{c}'
last = False
try:
if d == 'wmo':
limit = list(config.alert.map[type][alert].split(' '))
else:
limit = int(config.alert.map[type][alert])
except:
continue
for idx, v in enumerate(l):
if d == 'high':
if v >= limit:
hours += 1
if last and v > last:
code, value, last, stamp = c, v, v, ll[idx]
elif not last:
code, value, last, stamp = c, v, v, ll[idx]
elif d == 'low':
if v <= limit:
hours += 1
if last and v < last:
code, value, last, stamp = c, v, v, ll[idx]
elif not last:
code, value, last, stamp = c, v, v, ll[idx]
elif d == 'wmo':
for wmo in limit:
if v == int(wmo):
hours += 1
if last and v > last:
code, value, last, stamp = c, v, v, ll[idx]
elif not last:
code, value, last, stamp = c, v, v, ll[idx]
if hours:
break
# Check alert code
if code != 0:
icon = f'{icon}{code}'
time = conv.time('time', stamp)
if prop == 'condition':
icon = f'{config.map_alert_condition.get(value)}{code}'
value = config.localization.wmo.get(f'{value}d')
else:
value, unit = conv.item(value, unit)
# Notification Queue
if self.mode == 'msgqueue':
if code == 1 and utils.setting(f'alert_{prop}_notice', 'bool'):
config.addon.msgqueue.append([ f'{config.loc.short} - {utils.locaddon(32340)} ({hours} {utils.locaddon(32288)})', f'({time}) {name}: {value}{unit}', f'{config.addon_icons}/alert/{icon}.png' ])
elif code == 2 and utils.setting(f'alert_{prop}_caution', 'bool'):
config.addon.msgqueue.append([ f'{config.loc.short} - {utils.locaddon(32341)} ({hours} {utils.locaddon(32288)})', f'({time}) {name}: {value}{unit}', f'{config.addon_icons}/alert/{icon}.png' ])
elif code == 3 and utils.setting(f'alert_{prop}_danger', 'bool'):
config.addon.msgqueue.append([ f'{config.loc.short} - {utils.locaddon(32342)} ({hours} {utils.locaddon(32288)})', f'({time}) {name}: {value}{unit}', f'{config.addon_icons}/alert/{icon}.png' ])
return
# Set alert properties
utils.log(f'Updating alert: {prop} = {code}', 3)
config.addon.alerts += 1
utils.addprop(f'alert.{prop}', code)
utils.addprop(f'alert.{prop}.name', name)
utils.addprop(f'alert.{prop}.time', time)
utils.addprop(f'alert.{prop}.hours', hours)
utils.addprop(f'alert.{prop}.icon', f'{config.addon_icons}/alert/{icon}.png')
utils.addprop(f'alert.{prop}.value', value)
utils.addprop(f'alert.{prop}.unit', unit)
if code == 1:
utils.addprop(f'alert.{prop}.status', utils.locaddon(32340))
elif code == 2:
utils.addprop(f'alert.{prop}.status', utils.locaddon(32341))
elif code == 3:
utils.addprop(f'alert.{prop}.status', utils.locaddon(32342))
else:
if self.mode == 'msgqueue':
return
utils.addprop(f'alert.{prop}', 0)
for winprop in winprops:
utils.addprop(f'alert.{prop}.{winprop}', '')
### SET QUEUE
def msgqueue(self, type):
# Data
self.data[type] = utils.getfile(f'{config.loc.id}/{type}.json')
if not self.data[type]:
utils.log(f'No {type} data for location {config.loc.id}', 2)
return
# Index
indexnow = utils.index("now", self.data[type])
# Update msgqueue
for map in config.map.get(type):
# Alert
if map[3] == 'graph':
self.setalert(type, [ map, indexnow ])
### SET MULTI
def setmulti(self, src, map):
data = self.data[src]
time = map[1]
idx = map[2]
max = map[3]
min = map[4]
prop = map[5]
if config.addon.api is False and ( prop == 'hourly' or prop == 'daily' ):
count = 1
else:
count = 0
if not idx:
utils.log('Index invalid, weather data is not up to date ...', 3)
return
for index in range(idx, idx + max, 1):
map[0][2][0] = prop
self.setmap(src, map[0], index, count)
count += 1
count = -1
for index in range(idx - 1, idx - min, -1):
map[0][2][0] = prop
self.setmap(src, map[0], index, count)
count -= 1
### SET MAP
def setmap(self, src, map, idx=None, count=None, locid=None):
data = self.data[src]
# Property
if idx is not None:
if map[2][0] == 'day':
property = f'{map[2][0]}{count}.{map[2][1]}'
else:
property = f'{map[2][0]}.{count}.{map[2][1]}'
else:
if locid:
property = f'{map[2][0]}.{locid}.{map[2][1]}'
else:
property = f'{map[2][0]}.{map[2][1]}'
# Content
try:
content = utils.getprop(data, map, idx, count)
except TypeError as e:
utils.log(f'{property}: {type(e).__name__} {e}', 4)
utils.addprop(property, '')
except Exception as e:
utils.log(f'{property}: {type(e).__name__} {e}', 3)
utils.addprop(property, '')
else:
utils.addprop(property, content)
### GET MAP
def getmap(self, type):
# Layers disabled
if not type == 'osm':
if not utils.setting(f'map{type}', 'bool') or not utils.setting(f'loc{config.loc.id}maps', 'bool'):
return
# Check connectivity
if not api.network():
utils.log(f'[LOC{config.loc.id}] No network connectivity, maps not available ...', 3)
return
# Download
utils.log(f'[LOC{config.loc.id}] Downloading map: {type}', 3)
map = []
x, y = utils.lat2coords(config.loc.lat, config.loc.lon, config.addon.mapzoom)
tiles = [ [ x-1, y-1, 0, 0 ], [ x, y-1, 256, 0 ], [ x+1, y-1, 512, 0 ], [ x-1, y, 0, 256 ], [ x, y, 256, 256 ], [ x+1, y, 512, 256 ], [ x-1, y+1, 0, 512 ], [ x, y+1, 256, 512 ], [ x+1, y+1, 512, 512 ] ]
config.mapcache[type] = {}
# RV Index
if type.startswith('rv'):
time, path = api.getrvindex(type)
if time is None or path is None:
utils.log(f'[LOC{config.loc.id}] RVIndex {type} currently not available ...', 3)
return
# Other
else:
time = utils.dt('nowutcstamp')
path = None
# Queue
for count in range(0,9):
s, w, n, e = utils.coords2bbox(tiles[count][0], tiles[count][1], config.addon.mapzoom)
map.append([ config.loc.id, type, count, config.addon.mapzoom, tiles[count][0], tiles[count][1], tiles[count][2], tiles[count][3], path, time, s, w, n, e ])
# Download
with ThreadPoolExecutor(3) as pool:
pool.map(api.getmap, map)
# Merge
api.mapmerge(map)
# Cleanup
config.mapcache[type] = {}
dir = f'{config.addon_cache}/{config.loc.id}'
files = sorted(list(Path(dir).glob(f'{type}_*')), reverse=True)
history = config.addon.maphistory
for idx in range(0,100):
try:
file = files[idx]
except:
break
else:
if idx >= history:
utils.log(f'[LOC{config.loc.id}] Removing old map: {file.stem}', 3)
os.remove(file)
### PROPERTIES
def setother(self):
# Maps
if config.addon.api:
index = 0
else:
index = 1
for layer in config.map_layers:
# Layers disabled
if not utils.setting(f'map{layer}', 'bool') or not utils.setting(f'loc{config.loc.id}maps', 'bool'):
for item in [ 'area', 'layer', 'heading', 'time', 'legend' ]:
utils.addprop(f'Map.{index}.{item}', '')
index += 1
continue
# Files
dir = f'{config.addon_cache}/{config.loc.id}'
files = sorted(list(Path(dir).glob(f'{layer}_*')), reverse=True)
history = config.addon.maphistory
# Area
if files:
ut = int(files[0].stem.split('_')[1])
tz = utils.dt('stamploc', ut)
date = tz.strftime(config.kodi.date)
time = tz.strftime(config.kodi.time)
utils.addprop(f'Map.{index}.Area', f'{dir}/osm.png')
utils.addprop(f'Map.{index}.Layer', f'{dir}/{layer}_{ut}.png')
utils.addprop(f'Map.{index}.Heading', config.localization.layers.get(layer))
utils.addprop(f'Map.{index}.Time', f'{date} {time}')
utils.addprop(f'Map.{index}.Legend', '')
else:
for item in [ 'area', 'layer', 'heading', 'time', 'legend' ]:
utils.addprop(f'Map.{index}.{item}', '')
# Layers
for idx in range(0, history):
try:
file = files[idx]
except:
utils.addprop(f'Map.{index}.Layer.{idx}', '')
utils.addprop(f'Map.{index}.Time.{idx}', '')
else:
ut = int(file.stem.split('_')[1])
tz = utils.dt('stamploc', ut)
date = tz.strftime(config.kodi.date)
time = tz.strftime(config.kodi.time)
utils.addprop(f'Map.{index}.Layer.{idx}', f'{dir}/{layer}_{ut}.png')
utils.addprop(f'Map.{index}.Time.{idx}', f'{date} {time}')
index += 1
# Locations
utils.addprop('current.location', config.loc.name)
utils.addprop('location', config.loc.name)
self.setlocs()
# Fetched
for prop in [ 'current', 'weather', 'hourly', 'daily', 'timeofday', 'map' ]:
utils.addprop(f'{prop}.isfetched', 'true')
# Other
utils.addprop('alerts', config.addon.alerts)
if config.addon.api:
utils.addprop('icons', config.addon.icons)
utils.addprop('iconsdir', config.addon_icons)
utils.addprop('Provider', 'open-meteo.com, rainviewer.com, weather.gc.ca, met.no')
utils.addprop('ProviderLogo', f'{config.addon_path}/resources/banner.png')
else:
utils.addprop('WeatherProvider', 'open-meteo.com, rainviewer.com, weather.gc.ca, met.no')
utils.addprop('WeatherProviderLogo', f'{config.addon_path}/resources/banner.png')
### NOTIFICATION
def notification(self):
queue = config.addon.msgqueue
duration = utils.setting('alert_duration', 'int')
if queue:
for alert in queue:
utils.notification(alert[0], alert[1], alert[2], config.loc.id)
utils.monitor.waitForAbort(duration)
if utils.monitor.abortRequested():
utils.log(f'Abort requested ...', 3)
break