import os import sys import xbmc import xbmcgui import xbmcaddon import math import json import time import xml.etree.ElementTree as ET from datetime import datetime from pytz import timezone from pathlib import Path from . import config from . import conv from . import weather from . import monitor # Monitor monitor = monitor.Main() # Logging def log(msg, level=0): if level == 1: xbmc.log(msg=f'[weather.openmeteo]: [W] {msg}', level=xbmc.LOGINFO) elif level == 2: xbmc.log(msg=f'[weather.openmeteo]: [E] {msg}', level=xbmc.LOGINFO) elif level == 3: if config.addon.debug: xbmc.log(msg=f'[weather.openmeteo]: [D] {msg}', level=xbmc.LOGINFO) elif level == 4: if config.addon.verbose: xbmc.log(msg=f'[weather.openmeteo]: [V] {msg}', level=xbmc.LOGINFO) else: xbmc.log(msg=f'[weather.openmeteo]: [I] {msg}', level=xbmc.LOGINFO) # Setting def setting(arg, type='str', cache=False): # Cache if cache: try: value = config.addon.settings[arg] except: value = xbmcaddon.Addon().getSetting(arg) else: value = xbmcaddon.Addon().getSetting(arg) # Type if type == 'int': return int(value) elif type == 'float': return float(value) elif type == 'bool': if value == 'true': return True else: return False else: return str(value) def setsetting(arg, value): xbmcaddon.Addon().setSetting(arg, str(value)) def settingrpc(setting): try: r = json.loads(xbmc.executeJSONRPC('{{"jsonrpc":"2.0","id":1,"method":"Settings.GetSettingValue", "params": {{"setting": "{}"}} }}'.format(setting))) except: return None else: return r.get('result').get('value') def settings(changed=False): dict = {} skip = [ 'alert_notification', 'service', 'geoip' ] try: tree = ET.parse(f'{config.addon_data}settings.xml') except: return dict else: root = tree.getroot() for item in root: id = item.attrib['id'] if changed: if not 'loc' in id and not id in skip: dict[id] = item.text else: dict[id] = item.text return dict def region(arg): return xbmc.getRegion(arg) # Localization def loc(arg): return xbmc.getLocalizedString(arg) def locaddon(arg): return xbmcaddon.Addon().getLocalizedString(arg) # Notification def notification(header, msg, icon, locid): log(f'[LOC{locid}] Notification: {header} - {msg}') duration = (int(setting('alert_duration')) - 2) * 1000 xbmcgui.Dialog().notification(header, msg, icon, int(duration)) # Datetime def dt(arg, stamp=0): if arg == 'stamputc': return datetime.fromtimestamp(int(stamp), tz=timezone('UTC')) elif arg == 'stamploc': if config.loc.utz: return datetime.fromtimestamp(int(stamp), tz=timezone('UTC')).astimezone(config.loc.tz) else: return datetime.fromtimestamp(int(stamp), tz=timezone('UTC')).astimezone() elif arg == 'nowutc': return datetime.now(tz=timezone('UTC')) elif arg == 'nowutcstamp': return int(datetime.now(tz=timezone('UTC')).timestamp()) elif arg == 'nowloc': if config.loc.utz: return datetime.now(tz=timezone('UTC')).astimezone(config.loc.tz) else: return datetime.now(tz=timezone('UTC')).astimezone() elif arg == 'isoutc': return datetime.fromisoformat(stamp) elif arg == 'isoloc': if config.loc.utz: return datetime.fromisoformat(stamp).astimezone(config.loc.tz) else: return datetime.fromisoformat(stamp).astimezone() # Last update def lastupdate(arg): try: time1 = setting(arg) time2 = dt('nowutcstamp') return int(time2) - int(time1) except: return 321318000 def setupdate(arg): setsetting(arg, dt('nowutcstamp')) # Window property def clrprop(property): log(f'CLR: {property}', 4) xbmcgui.Window(12600).clearProperty(property) def winprop(property): log(f'GET: {property}', 4) return xbmcgui.Window(12600).getProperty(property) # Window property (Set) def setprop(property, data): log(f'SET: {property} = {data}', 4) xbmcgui.Window(12600).setProperty(property, str(data)) # Window property (Get) def getprop(data, map, idx, count): # Content if len(map[1]) == 1: if idx is not None: content = data[map[1][0]][idx] else: content = data[map[1][0]] elif len(map[1]) == 2: if idx is not None: content = data[map[1][0]][map[1][1]][idx] else: content = data[map[1][0]][map[1][1]] elif len(map[1]) == 3: if idx is not None: content = data[map[1][0]][map[1][1]][map[1][2]][idx] else: content = data[map[1][0]][map[1][1]][map[1][2]] if content is None: raise TypeError('No data') # Unit unit = map[3] # WMO (isday) if unit.startswith('wmo') or unit == 'image' or unit == 'code': if idx: try: if data[map[1][0]]['is_day'][idx] == 1: isday = 'd' else: isday = 'n' except: isday = 'd' else: try: if data[map[1][0]]['is_day'] == 1: isday = 'd' else: isday = 'n' except: isday = 'd' # Tools if unit == 'round': content = int(round(content)) elif unit == 'roundpercent': content = f'{int(round(content))}%' elif unit == 'round2': content = round(content, 2) elif unit == 'wmocond': content = config.localization.wmo.get(f'{content}{isday}') elif unit == 'wmoimage': content = f'{config.addon_icons}/{config.addon.icons}/{content}{isday}.png' elif unit == 'wmocode': content = f'{content}{isday}' elif unit == 'image': # KODI workaround for DayX.OutlookIcon, add "resource://resource.images.weathericons.default" to path if map[2][0] == 'day': content = f'resource://resource.images.weathericons.default/{config.map_wmo.get(f"{content}{isday}")}.png' else: content = f'{config.map_wmo.get(f"{content}{isday}")}.png' elif unit == 'code': content = config.map_wmo.get(f'{content}{isday}') elif unit == 'date': content = dt('stamploc', content).strftime(config.kodi.date) elif unit == 'time': content = conv.time('time', content) elif unit == 'timeiso': content = conv.time('timeiso', content) elif unit == 'hour': content = conv.time('hour', content) elif unit == 'weekday': content = config.localization.weekday.get(dt('stamploc', content).strftime('%u')) elif unit == 'weekdayshort': content = config.localization.weekdayshort.get(dt('stamploc', content).strftime('%u')) elif unit == '%': content = f'{content}%' # Temperature elif unit == 'temperature': content = conv.temp(content) elif unit == 'temperaturekodi': content = conv.temp(content, True) elif unit == 'temperatureunit': content = f'{conv.temp(content)}{conv.temp()}' elif unit == 'unittemperature': content = conv.temp() # Speed elif unit == 'speed': content = conv.speed(content) elif unit == 'unitspeed': content = conv.speed() # Precipitation elif unit == 'precipitation': content = conv.precip(content) elif unit == 'unitprecipitation': content = conv.precip() # Distance elif unit == 'distance': content = conv.distance(content) elif unit == 'unitdistance': content = conv.distance() # UVIndex elif unit == 'uvindex': content = conv.dp(content, config.addon.uvindexdp) # Particles elif unit == 'particles': content = conv.dp(content, config.addon.particlesdp) elif unit == 'unitparticles': content = 'μg/m³' # Pollen elif unit == 'pollen': content = conv.dp(content, config.addon.pollendp) elif unit == 'unitpollen': content = f'{locaddon(32456)}/m³' # Radiation elif unit == 'radiation': content = conv.dp(content, config.addon.radiationdp) elif unit == 'unitradiation': content = 'W/m²' # Pressure elif unit == 'pressure': content = conv.pressure(content) elif unit == 'unitpressure': content = conv.pressure() # Direction elif unit == 'direction': content = conv.direction(content) # Percent elif unit == 'unitpercent': content = '%' # Wind elif unit == 'windkodi': speed = round(conv.speed(data['current']['wind_speed_10m']), True) unit = conv.speed(False, True) direction = conv.direction(data['current']['wind_direction_10m']) content = loc(434).format(direction, int(speed), unit) # Moonphase elif unit == 'moonphase': content = conv.moonphase(int(content)) elif unit == 'moonphaseimage': content = f'{config.addon_icons}/moon/{conv.moonphaseimage(int(content))}' # Graphs elif unit == 'graph': property = f'{map[2][0]}.{count}.{map[2][1]}' time = map[2][0] type = map[2][1] mscale = map[4] alert = 0 scale = 0 scaleneg = False # Content try: calc = map[5] except: calc = False else: if calc == 'temperature': if conv.temp() == '°F': mscale = '100' elif calc == 'divide10': content = content/10 elif calc == 'divide1000': content = content/1000 elif calc == 'pressure': content = config.map_pressure.get(int(content),0) # Autoscale try: ascale = config.addon.scalecache[f'{time}{type}'] nscale = config.addon.scalecache[f'{time}{type}neg'] except: _idx_ = idx for _count_ in range(0,25): if calc == 'temperature': v = conv.temp(data[map[1][0]][map[1][1]][_idx_+_count_], True) # Negative temperature if v < 0: scaleneg = True check = abs(v) elif calc == 'divide10': check = abs(data[map[1][0]][map[1][1]][_idx_+_count_]/10) elif calc == 'divide1000': check = abs(data[map[1][0]][map[1][1]][_idx_+_count_]/1000) elif calc == 'pressure': check = abs(config.map_pressure.get(int(data[map[1][0]][map[1][1]][_idx_+_count_]))) else: check = abs(data[map[1][0]][map[1][1]][_idx_+_count_]) if check > scale: scale = check if scale < 1: config.addon.scalecache[f'{time}{type}'] = 1 elif scale >= 101 and scale <= 150: config.addon.scalecache[f'{time}{type}'] = 150 elif scale >= 151 and scale <= 200: config.addon.scalecache[f'{time}{type}'] = 200 else: config.addon.scalecache[f'{time}{type}'] = math.ceil(scale/10.0)*10 # Negative values config.addon.scalecache[f'{time}{type}neg'] = scaleneg # Set cache ascale = config.addon.scalecache[f'{time}{type}'] nscale = config.addon.scalecache[f'{time}{type}neg'] log(f'Scale {type} ({time}) = {ascale}', 4) # Alert for _alert_ in config.alert.map[type]: if not 'alert' in _alert_: continue if 'wmo' in _alert_: limit = list(config.alert.map[type][_alert_].split(' ')) else: limit = float(config.alert.map[type][_alert_]) if not limit: continue if 'high' in _alert_: if content >= int(limit): alert = int(_alert_[-1]) elif 'low' in _alert_: if content <= int(limit): alert = int(_alert_[-1]) elif 'wmo' in _alert_: for wmo in limit: if content == int(wmo): alert = int(alert[-1]) setprop(f'{property}alert', alert) # Content if calc == 'temperature': content = conv.temp(content, True) if ascale == 1: content = round(content,1) else: content = round(content) # Set properties if nscale: setprop(f'{property}image', f'{config.addon_icons}/graph/{config.kodi.height}/scaleneg{mscale}_{content}.png') setprop(f'{property}imagescale', f'{config.addon_icons}/graph/{config.kodi.height}/scaleneg{ascale}_{content}.png') setprop(f'{property}scale', f'{ascale}n') else: setprop(f'{property}image', f'{config.addon_icons}/graph/{config.kodi.height}/scale{mscale}_{content}.png') setprop(f'{property}imagescale', f'{config.addon_icons}/graph/{config.kodi.height}/scale{ascale}_{content}.png') setprop(f'{property}scale', f'{ascale}') # Color if alert == 0: if content < 0: setprop(f'{property}color', config.addon.cnegative) setprop(f'{property}colornormal', config.addon.cnegative) else: setprop(f'{property}color', config.addon.cdefault) setprop(f'{property}colornormal', config.addon.cnormal) elif alert == 1: setprop(f'{property}color', config.addon.cnotice) setprop(f'{property}colornormal', config.addon.cnotice) elif alert == 2: setprop(f'{property}color', config.addon.ccaution) setprop(f'{property}colornormal', config.addon.ccaution) elif alert == 3: setprop(f'{property}color', config.addon.cdanger) setprop(f'{property}colornormal', config.addon.cdanger) # Return data return content # Set alert def setalert(data, map, idx, locid, curid, loc, mode): winprops = [ 'name', 'value', 'icon', 'unit', 'time', 'hours', 'status' ] type = map[2][1] prop = config.alert.map[type]['type'] name = locaddon(config.alert.map[type]['loc']) shours = config.addon.alerthours hours = { '1': 0, '2': 0, '3': 0 } code = 0 value = 0 unit = '' log(f'[LOC{locid}] Checking alert: {prop}', 3) for index in range(idx, idx+shours): try: content = int(data[map[1][0]][map[1][1]][index]) except: if locid == curid: setprop(f'alert.{prop}', 0) for winprop in winprops: setprop(f'alert.{prop}.{winprop}', '') return # Alert for alert in config.alert.map[type]: if not 'alert' in alert: continue if 'wmo' in alert: limit = list(config.alert.map[type][alert].split(' ')) else: limit = int(config.alert.map[type][alert]) if not limit: continue if 'high' in alert: if content >= limit: hours[f'{alert[-1]}'] += 1 if content >= value: if content >= limit: code = int(alert[-1]) value = content stamp = data[map[1][0]]['time'][index] elif 'low' in alert: if content <= limit: hours[f'{alert[-1]}'] += 1 if content <= value: if content <= limit: code = int(alert[-1]) value = content stamp = data[map[1][0]]['time'][index] elif 'wmo' in alert: for wmo in limit: if content == int(wmo): hours[f'{alert[-1]}'] += 1 if content > value: code = int(alert[-1]) value = content stamp = data[map[1][0]]['time'][index] # Check alert code if code != 0: icon = f'{prop}{code}' time = conv.time('time', stamp) if prop == 'temperature': value = conv.temp(value) unit = conv.temp() elif prop == 'windspeed' or prop == 'windgust': value = conv.speed(value) unit = conv.speed() elif prop == 'condition': icon = f'condition{config.map_alert_condition.get(value)}{code}' value = config.localization.wmo.get(f'{value}d') # Set alert properties for current location if locid == curid: if setting(f'alert_{prop}_enabled', 'bool'): log(f'[LOC{locid}] Updating alert: {prop} = {code}', 3) config.addon.alerts += 1 setprop(f'alert.{prop}', code) setprop(f'alert.{prop}.name', name) setprop(f'alert.{prop}.time', time) setprop(f'alert.{prop}.hours', hours[str(code)]) setprop(f'alert.{prop}.icon', f'{config.addon_icons}/alert/{icon}.png') setprop(f'alert.{prop}.value', value) setprop(f'alert.{prop}.unit', unit) if code == 1: setprop(f'alert.{prop}.status', locaddon(32340)) elif code == 2: setprop(f'alert.{prop}.status', locaddon(32341)) elif code == 3: setprop(f'alert.{prop}.status', locaddon(32342)) else: setprop(f'alert.{prop}', '') for winprop in winprops: setprop(f'alert.{prop}.{winprop}', '') # Notification if mode == 'msgqueue': if code == 1 and setting(f'alert_{prop}_notice', 'bool'): config.addon.msgqueue.append([ f'{loc} - {locaddon(32340)} ({hours[str(code)]} {locaddon(32288)})', f'({time}) {name}: {value} {unit}', f'{config.addon_icons}/alert/{icon}.png' ]) elif code == 2 and setting(f'alert_{prop}_caution', 'bool'): config.addon.msgqueue.append([ f'{loc} - {locaddon(32341)} ({hours[str(code)]} {locaddon(32288)})', f'({time}) {name}: {value} {unit}', f'{config.addon_icons}/alert/{icon}.png' ]) elif code == 3 and setting(f'alert_{prop}_danger', 'bool'): config.addon.msgqueue.append([ f'{loc} - {locaddon(32342)} ({hours[str(code)]} {locaddon(32288)})', f'({time}) {name}: {value} {unit}', f'{config.addon_icons}/alert/{icon}.png' ]) else: if locid == curid: setprop(f'alert.{prop}', 0) for winprop in winprops: setprop(f'alert.{prop}.{winprop}', '') # Get file def getfile(file): try: # Note: Changing language throws an exception without enforcing utf8 with open(Path(f'{config.addon_cache}/{file}'), 'r', encoding='utf8') as f: data = json.load(f) except Exception as e: log(f'{e}', 2) return None else: return data # Index def index(arg, data): if arg == 'now': match = dt('nowloc').strftime('%Y-%m-%d %H') elif arg == 'mid': match = dt('nowloc').strftime('%Y-%m-%d 00') elif arg == 'day': match = dt('nowloc').strftime('%Y-%m-%d') for idx in range(config.mindata, config.maxdata): try: if arg == 'day': timecheck = dt('stamploc', data['daily']['time'][idx]).strftime('%Y-%m-%d') else: timecheck = dt('stamploc', data['hourly']['time'][idx]).strftime('%Y-%m-%d %H') except: return None else: if timecheck == match: return idx # Directory def createdir(): file = config.addon_data + 'w.txt' try: os.makedirs(config.addon_cache, exist_ok=True) with open(file, 'w') as f: f.write('w') except Exception as e: log(f'Addon data directory not writeable: {config.addon_data} {e}', 2) xbmcgui.Dialog().notification('Weather Open-Meteo', 'Weather data directory not writeable, check log ...', xbmcgui.NOTIFICATION_ERROR, 15000) sys.exit(1) else: os.remove(file) # Locations def locations(): locs = 0 for count in range(1,6): loc = setting(f'loc{count}') if loc: setprop(f'location{count}', loc) locs += 1 setprop('locations', locs) # LatLon2Coords def lat2coords(lat_deg, lon_deg, zoom): lat_rad = math.radians(lat_deg) n = 1 << zoom xtile = int((lon_deg + 180.0) / 360.0 * n) ytile = int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n) return xtile, ytile def numTiles(z): return(pow(2,z)) def latEdges(y,z): n = numTiles(z) unit = 1 / n relY1 = y * unit relY2 = relY1 + unit lat1 = mercatorToLat(math.pi * (1 - 2 * relY1)) lat2 = mercatorToLat(math.pi * (1 - 2 * relY2)) return(lat1,lat2) def lonEdges(x,z): n = numTiles(z) unit = 360 / n lon1 = -180 + x * unit lon2 = lon1 + unit return(lon1,lon2) def mercatorToLat(mercatorY): return(math.degrees(math.atan(math.sinh(mercatorY)))) def coords2bbox(x,y,z): lat1,lat2 = latEdges(y,z) lon1,lon2 = lonEdges(x,z) return((lat2, lon1, lat1, lon2)) # S,W,N,E