def psychrochart_config(): if request.method == 'GET': if not has_var(redis, 'chart_style'): task = celery.send_task(TASK_CLEAN_CACHE_DATA) return json_error(404000, error_msg=f"No chart config available!, " f"resetting all (task: {task})") styles = get_var(redis, 'chart_style') styles['zones'] = get_var(redis, 'chart_zones')['zones'] return json_response(styles) elif isinstance(request.json, dict) and request.json: new_data = request.json logging.warning(f"Set new chart style: {new_data}") styles = get_var(redis, 'chart_style') zones = get_var(redis, 'chart_zones') _update_dict(styles, new_data, CHART_STYLE_KEYS) _update_dict(zones, new_data, CHART_STYLE_KEYS) set_var(redis, 'chart_style', styles) set_var(redis, 'chart_zones', zones) set_var(redis, 'chart_config_changed', True) logging.debug('Make psychrochart now!') celery.send_task(TASK_CREATE_PSYCHROCHART) styles['zones'] = get_var(redis, 'chart_zones')['zones'] return json_response({"new_config": new_data, "result": styles}) return json_error(400, error_msg="Bad request! json: %s; args: %s", msg_args=[request.json, request.args])
def get_homeassistant_sensors_evolution(): ha_evolution = get_var(redis, 'ha_evolution') if ha_evolution: # Without response schema (direct use with HA REST sensor) return jsonify(ha_evolution) # Do something! return json_error(500002, error_msg="No history data available!")
def homeassistant_states(): if not has_var(redis, 'ha_states'): return json_error(404002, error_msg="No Home Assistant states available!") ha_states = get_var(redis, 'ha_states', unpickle_object=True) for s in ha_states: ha_states[s]['last_updated'] = ha_states[s]['last_updated'].isoformat() ha_states[s]['last_changed'] = ha_states[s]['last_changed'].isoformat() return json_response(ha_states)
def homeassistant_config(): if request.method == 'GET': if not has_var(redis, 'ha_yaml_config'): return json_error(404001, error_msg="No Home Assistant config available!, " "please POST one") return json_response(get_var(redis, 'ha_yaml_config')) elif isinstance(request.json, dict) and request.json: new_data = request.json logging.warning(f"Set new HA config: {new_data}") ha_config = get_var(redis, 'ha_yaml_config') _update_dict(ha_config, new_data, HA_CONFIG_KEYS) set_var(redis, 'ha_yaml_changed', True) set_var(redis, 'ha_yaml_config', ha_config) celery.send_task(TASK_RELOAD_HA_CONFIG) return json_response(ha_config) return json_error(400, error_msg="Bad request! json: %s; args: %s", msg_args=[request.json, request.args])
def get_ha_states(redis): api = get_var(redis, 'ha_api', unpickle_object=True) if not api: logging.error(f"No HA API loaded, aborting get_states") if has_var(redis, 'ha_states'): remove_var(redis, 'ha_states') return {} sensors = get_var(redis, 'ha_sensors') logging.debug(f"Sensors: {sensors}") entities = [] if "pressure_sensor" in sensors: entities.append(sensors["pressure_sensor"]) # if "sun" in sensors: # entities.append(sensors["sun"]) if "interior" in sensors: entities += [ s for sensor in sensors["interior"].values() for k, s in sensor.items() if k in ['temperature', 'humidity'] ] if "exterior" in sensors: entities += [ s for sensor in sensors["exterior"].values() for k, s in sensor.items() if k in ['temperature', 'humidity'] ] # print(entities) try: states = { s.entity_id: s.as_dict() for s in filter(lambda x: x.entity_id in entities, get_states(api)) } set_var(redis, 'ha_states', states, pickle_object=True) except (ReadTimeoutError, ConnectionRefusedError, HomeAssistantError): states = {} return states
def periodic_get_ha_states(): """Background task to update the HA sensors states.""" making_chart_now = get_var(redis, 'making_chart_now', default=0) if making_chart_now: logging.warning('last periodic_get_ha_states is not finished. ' 'Aborting this try...') return set_var(redis, 'making_chart_now', 1) _log_task_init("periodic_get_ha_states") _load_homeassistant_config() if not has_var(redis, 'ha_api'): get_ha_api(redis) logging.debug('loading states...') states = get_ha_states(redis) if not states: logging.error(f"Can't load HA states!") set_var(redis, 'making_chart_now', 0) return logging.debug('making points...') make_points_from_states(redis, states) logging.debug('making chart...') ok = make_psychrochart(redis) logging.debug('chart DONE') set_var(redis, 'making_chart_now', 0) if ok and get_var(redis, 'ha_yaml_changed'): # HA Configuration changed, and the result is OK, saving it now logging.warning('Saving HA config to disk ' '(after producing successfully one chart)') save_homeassistant_config(get_var(redis, 'ha_yaml_config')) remove_var(redis, 'ha_yaml_changed') remove_var(redis, 'ha_yaml_config') _load_homeassistant_config() if ok and get_var(redis, 'chart_config_changed'): # HA Configuration changed, and the result is OK, saving it now logging.warning('Saving PsychroChart config to disk ' '(after producing successfully one chart)') save_chart_style(get_var(redis, 'chart_style')) save_chart_zones(get_var(redis, 'chart_zones')) remove_var(redis, 'chart_config_changed') remove_var(redis, 'chart_style') remove_var(redis, 'chart_zones') _load_chart_config() return True
def init_chart_config(sender, **kwargs): # from psychrochartmaker import TASK_PERIODIC_GET_HA_STATES from psychrochartmaker import TASK_CLEAN_CACHE_DATA from psychrochartmaker.tasks import periodic_get_ha_states logging.warning(f"On INIT_CHART_CONFIG") task = celery.send_task(TASK_CLEAN_CACHE_DATA) task.get() # Program HA polling schedule ha_history = get_var(redis, 'ha_history') scheduler = sender.add_periodic_task(ha_history['scan_interval'], periodic_get_ha_states.s(), name='HA sensor update') logging.info(f'DEBUG scheduler: {scheduler}') set_var(redis, 'scheduler', scheduler) # Make first psychrochart celery.send_task('create_psychrochart') return True
def get_ha_api(redis): ha_config = get_var(redis, 'ha_config') logging.debug(f"HA API config: {ha_config}") # Get HA API api_params = dict(host=ha_config.get('host', '127.0.0.1'), api_password=ha_config.get('api_password', None), port=ha_config.get('port', 8123), use_ssl=ha_config.get('use_ssl', False)) try: api = API(**api_params) try: assert api.validate_api(force_validate=True) set_var(redis, 'ha_api', api, pickle_object=True) except AssertionError: logging.error(f"No HA API found. Removing config from cache") if has_var(redis, 'ha_api'): remove_var(redis, 'ha_api') except (HomeAssistantError, ConnectionError, NewConnectionError, MaxRetryError) as exc: logging.error(f"{exc.__class__}: {str(exc)}") return
def get_svg_chart(): svg = get_var(redis, 'svg_chart') if svg: return image_response(svg, image_type='svg') # Do something! return json_error(500001, error_msg="No SVG image available!")
def make_psychrochart(redis, altitude=None, pressure_kpa=None, points=None, connectors=None, arrows=None, interior_zones=None): """Create the PsychroChart SVG file and save it to disk.""" # Load chart style: chart_style = load_config(get_var(redis, 'chart_style')) zones = get_var(redis, 'chart_zones') if altitude is None: # Try redis key altitude = get_var(redis, 'altitude') if pressure_kpa is None: # Try redis key pressure_kpa = get_var(redis, 'pressure_kpa') if points is None: # Try redis key points = get_var(redis, 'last_points', default={}) if arrows is None: # Try redis key arrows = get_var(redis, 'arrows') if interior_zones is None: # Try redis key interior_zones = get_var(redis, 'interior_zones') p_label = '' if pressure_kpa is not None: chart_style['limits']['pressure_kpa'] = pressure_kpa p_label = 'P={:.1f} mb '.format(pressure_kpa * 10) chart_style['limits'].pop('altitude_m', None) logging.debug(f"using pressure: {pressure_kpa}") elif altitude is not None: chart_style['limits']['altitude_m'] = altitude p_label = 'H={:.0f} m '.format(altitude) # Make chart # chart = PsychroChart(chart_style, zones, logger=app.logger) chart = PsychroChart(chart_style, zones) # Append lines t_min, t_opt, t_max = 16, 23, 30 chart.plot_vertical_dry_bulb_temp_line(t_min, { "color": [0.0, 0.125, 0.376], "lw": 2, "ls": ':' }, ' TOO COLD, {:g}°C'.format(t_min), ha='left', loc=0., fontsize=14) chart.plot_vertical_dry_bulb_temp_line(t_opt, { "color": [0.475, 0.612, 0.075], "lw": 2, "ls": ':' }) chart.plot_vertical_dry_bulb_temp_line(t_max, { "color": [1.0, 0.0, 0.247], "lw": 2, "ls": ':' }, 'TOO HOT, {:g}°C '.format(t_max), ha='right', loc=1, reverse=True, fontsize=14) # Append pressure / altitude label if p_label: chart.axes.annotate(p_label, (1, 0), xycoords='axes fraction', ha='right', va='bottom', fontsize=15, color='darkviolet') if arrows: chart.plot_arrows_dbt_rh(arrows) # Append history label points_dq = get_var(redis, 'deque_points', default=[], unpickle_object=True) if len(points_dq) > 2: start = list(points_dq[0].values())[0] end = list(points_dq[-1].values())[0] delta = (dt.datetime.fromtimestamp(end['ts']) - dt.datetime.fromtimestamp(start['ts'])).total_seconds() # delta = history_config['delta_arrows'] chart.axes.annotate('∆T:{:.1f}h'.format(delta / 3600.), (0, 0), xycoords='axes fraction', ha='left', va='bottom', fontsize=10, color='darkgrey') if points: chart.plot_points_dbt_rh(points, connectors, convex_groups=interior_zones) chart.plot_legend(frameon=False, fontsize=15, labelspacing=.8, markerscale=.8) bytes_svg = BytesIO() chart.save(bytes_svg, format='svg') bytes_svg.seek(0) set_var(redis, 'svg_chart', bytes_svg.read()) set_var(redis, 'chart_axes', chart.axes, pickle_object=True) chart.remove_annotations() set_var(redis, 'chart', chart, pickle_object=True) return True
def _load_homeassistant_config(): yaml_config = get_var(redis, 'ha_yaml_config') if yaml_config is None: yaml_config = load_homeassistant_config() parse_config_ha(redis, yaml_config)
def make_points_from_states(redis, states): # Make points sensors = get_var(redis, 'ha_sensors') points = get_var(redis, 'last_points', default={}) points_unknown = get_var(redis, 'points_unknown', default=[]) for sensor_group in sensors.values(): if isinstance(sensor_group, str): try: set_var(redis, 'pressure_kpa', _mb2kpa(float(states[sensor_group]['state']))) except ValueError: logging.error(f"Bad pressure read from {sensor_group}") # pass continue for key, p_config in sensor_group.items(): try: points.update({ key: { 'xy': (float(states[p_config['temperature']]['state']), float(states[p_config['humidity']]['state'])), 'style': { 'marker': 'o', **p_config['style'] }, 'ts': (states[p_config['humidity']] ['last_updated'].timestamp()), 'label': key } }) if key in points_unknown: points_unknown.remove(key) except KeyError: logging.error( f"KeyError with {key} [sensor_group: {sensor_group}]") points_unknown.append(key) except ValueError: logging.warning( f"ERROR with {key} sensor [state: " f"{states[p_config['temperature']]['state']}ºC, " f"{states[p_config['humidity']]['state']}%]") points_unknown.append(key) set_var(redis, 'last_points', points) set_var(redis, 'points_unknown', points_unknown) # Make arrows history_config = get_var(redis, 'ha_history', default={}) if 'delta_arrows' not in history_config or \ not history_config['delta_arrows']: return delta_arrows = history_config['delta_arrows'] scan_interval = history_config['scan_interval'] len_deque = max(3, int(delta_arrows / scan_interval)) points_dq = get_var(redis, 'deque_points', default=deque([], maxlen=len_deque), unpickle_object=True) points_dq.append(points) set_var(redis, 'deque_points', points_dq, pickle_object=True) num_points_dq = len(points_dq) if num_points_dq > 1: # arrows = {k: [p['xy'], points_dq[0][k]['xy']] # for k, p in points.items() if k in points_dq[0] # and p != points_dq[0][k]} arrows = { k: { 'xy': [p['xy'], points_dq[0][k]['xy']], 'style': _arrow_style(p['style']) } for k, p in points.items() if k in points_dq[0] and p != points_dq[0][k] } # logging.info('MAKE ARROWS: %s', arrows) set_var(redis, 'arrows', arrows) # Make evolution JSON endpoint with history if num_points_dq > 3: ev_data = { "num_points": num_points_dq, "pressure_kPa": get_var(redis, 'pressure_kpa') } start_p = points_dq[0] mid_p = points_dq[num_points_dq // 2 - 1] end_p = points_dq[-1] ev_data.update({ key: _make_ev_data(start_p.get(key), mid_p.get(key), point) for key, point in end_p.items() }) logging.debug(f"EVOLUTION_DATA: {ev_data}") set_var(redis, 'ha_evolution', ev_data)