heating_control/heating_control.py

400 lines
15 KiB
Python

import json
from urllib.parse import urlparse, quote
import urllib.request
from datetime import datetime
import sys
import time
import subprocess
from functools import wraps
from statistics import mean
def retry(ExceptionToCheck, tries=20, delay=1, backoff=1.1, logger=None):
"""Retry calling the decorated function using an exponential backoff.
http://www.saltycrane.com/blog/2009/11/trying-out-retry-decorator-python/
original from: http://wiki.python.org/moin/PythonDecoratorLibrary#Retry
:param ExceptionToCheck: the exception to check. may be a tuple of
exceptions to check
:type ExceptionToCheck: Exception or tuple
:param tries: number of times to try (not retry) before giving up
:type tries: int
:param delay: initial delay between retries in seconds
:type delay: int
:param backoff: backoff multiplier e.g. value of 2 will double the delay
each retry
:type backoff: int
:param logger: logger to use. If None, print
:type logger: logging.Logger instance
"""
def deco_retry(f):
@wraps(f)
def f_retry(*args, **kwargs):
mtries, mdelay = tries, delay
while mtries > 1:
try:
return f(*args, **kwargs)
except ExceptionToCheck as e:
msg = "%s, Retrying in %d seconds..." % (str(e), mdelay)
if logger:
logger.warning(msg)
else:
print(msg)
time.sleep(mdelay)
mtries -= 1
mdelay *= backoff
return f(*args, **kwargs)
return f_retry # true decorator
return deco_retry
def heishamon_command(command, value):
subprocess.run(f'mosquitto_pub -t panasonic_heat_pump/commands/{command} -m {value} -u heishamon -P heishamon'.split(' '))
def heishamon_SetForceDHW():
heishamon_command('SetForceDHW', '1')
@retry(Exception)
def heishamon_json_load():
return json.load(urllib.request.urlopen('http://heishamon.local/json'))['heatpump']
@retry(Exception)
def heishamon_EnableHeat():
with urllib.request.urlopen('http://heishamon.local/command?SetOperationMode=4') as response:
print(response.url, response.read())
@retry(Exception)
def heishamon_EnableHeatOnly():
with urllib.request.urlopen('http://heishamon.local/command?SetOperationMode=0') as response:
print(response.url, response.read())
@retry(Exception)
def heishamon_DisableHeat():
with urllib.request.urlopen('http://heishamon.local/command?SetOperationMode=3') as response:
print(response.url, response.read())
@retry(Exception)
def heishamon_SetHeatRequestTemperature(new_shift):
print('-> set new water shift', new_shift)
with urllib.request.urlopen(f'http://heishamon.local/command?SetZ1HeatRequestTemperature={round(new_shift)}') as response:
print(response.url, response.read())
@retry(Exception)
def heishamon_SetDemandControl(value):
print('-> set demand control', value)
with urllib.request.urlopen(f'http://heishamon.local/command?SetDemandControl={value}') as response:
print(response.url, response.read())
def heishamon_SetDHWTemp(value):
print('-> set DHW temp', value)
heishamon_command('SetDHWTemp', value)
def influxquery(query):
req = urllib.request.Request(
url="https://influxdb.kanthaus.online:443/query?db=kanthaus&q=" + quote(query) + "&epoch=ms",
headers={
'Authorization': 'Basic a2FudGhhdXMtcmVhZGVyOlh6elZxa3lsUGZVMUMyT294elRU'
}
)
u = urllib.request.urlopen(req)
return json.load(u)
def interpolate(x, x0, x1, y0, y1):
lower = min(y0, y1)
upper = max(y0, y1)
return min(upper, max(lower, y0 + (x - x0)*(y1 - y0)/(x1 - x0)))
def compressor_current_to_demand_control(c):
return round(interpolate(c, 3, 6.4, 87, 149))
def optimum_cop(outdoor, water):
# CoP data from manual, WH-MXC12H9E8
from scipy.interpolate import LinearNDInterpolator
x0 = [7, 2, -7, -15]*4
x1 = [30]*4 + [35]*4 + [40]*4 + [45]*4
y= [5.5, 3.76, 3.12, 2.53,
4.74, 3.44, 2.72, 2.42,
4.05, 3.1, 2.41, 2.22,
3.54, 2.82, 2.17, 2.05]
interp = LinearNDInterpolator(list(zip(x0, x1)), y)
return float(interp(outdoor, water))
print('\n---------------')
print(datetime.now())
hot_water_mode = influxquery("""
SELECT max(valve_position_dhw)
FROM "housebus.heating.heating_status.1.0"
WHERE time >= now() - 29m
""")["results"][0]["series"][0]["values"][0][1]
compressor_freq = influxquery("""
SELECT mean(compressor_frequency)
FROM "housebus.heating.heating_status.1.0"
WHERE time >= now() - 1m
""")["results"][0]["series"][0]["values"][0][1]
ee_vals = [val for time, val in influxquery("""
SELECT mean("value") FROM "homeautomation.Obis"
WHERE (code = '16.7.0' and node_id = '4') AND time > now() - 10m
GROUP BY time(1m)
""")["results"][0]["series"][0]["values"] if val is not None]
electric_export = -mean(ee_vals + ee_vals[-5:] + ee_vals[-2:] + ee_vals[-1:])
heishamon_json = heishamon_json_load()
dhw_target_temp = int(heishamon_json[9]['Value'])
dhw_active = int(heishamon_json[20]['Value'])
dhw_temp = int(heishamon_json[10]['Value'])
if hot_water_mode > 0 and compressor_freq >= 19:
if dhw_active > 0:
print('hot water mode is active.')
if electric_export > -500:
print('sun is shining, set high dhw target')
if dhw_target_temp < 51:
heishamon_SetDHWTemp(51)
else:
print('sun is not shining, set low dhw target')
if dhw_target_temp > 40:
heishamon_SetDHWTemp(40)
sys.exit()
print('hot water mode was active. wait until system has heated up. quitting...')
sys.exit()
room_temp = influxquery("""
SELECT mean(temperature) / 100
FROM "homeautomation.Environment"
WHERE time >= now() - 5m AND (node_id = '15' OR node_id = '21' OR node_id = '19')
""")["results"][0]["series"][0]["values"][0][1]
previous_room_temp = influxquery("""
SELECT mean(temperature) / 100
FROM "homeautomation.Environment"
WHERE time >= now() - 35m AND time < now() - 30m AND (node_id = '15' OR node_id = '21' OR node_id = '19')
""")["results"][0]["series"][0]["values"][0][1]
outdoor_temp_24h = influxquery("""
SELECT mean(outdoor_temp)
FROM "housebus.heating.heating_status.1.0"
WHERE time >= now() - 24h
""")["results"][0]["series"][0]["values"][0][1]
outdoor_temp_10m = influxquery("""
SELECT mean(outdoor_temp)
FROM "housebus.heating.heating_status.1.0"
WHERE time >= now() - 10m
""")["results"][0]["series"][0]["values"][0][1]
compressor_current = influxquery("""
SELECT mean(compressor_current)
FROM "housebus.heating.heating_status.1.0"
WHERE time >= now() - 30m
AND valve_position_dhw = 0
AND defrost_running = 0
""")["results"][0]["series"][0]["values"][0][1]
shift = int(heishamon_json[27]['Value'])
if shift < -5 or shift > 5:
raise Exception('wrong shift, maybe heatpump is not running in heatshift mode?', shift)
flow_temp = influxquery("""
SELECT last(flow_temp)
FROM "housebus.heating.heating_status.1.0"
""")["results"][0]["series"][0]["values"][0][1]
flow_temp_before = influxquery("""
SELECT first(flow_temp)
FROM "housebus.heating.heating_status.1.0"
WHERE time >= now() - 30m
""")["results"][0]["series"][0]["values"][0][1]
flow_target_temp = int(heishamon_json[7]['Value'])
defrost = influxquery("""
SELECT max(defrost_running)
FROM "housebus.heating.heating_status.1.0"
WHERE time >= now() - 5m
""")["results"][0]["series"][0]["values"][0][1]
try:
solar_forecast = [val for time, val in influxquery("""
SELECT kw * 1000
FROM "solarforecast"
WHERE time >= now() and time < now() + 5h
""")["results"][0]["series"][0]["values"]]
except:
print('No solar forecast available!')
solar_forecast = [0, 0, 0, 0, 0]
electricity = influxquery("""
SELECT mean(power_mw) / 1000
FROM "housebus.electricity.meter.1.0"
WHERE time >= now() - 10m
""")["results"][0]["series"][0]["values"][0][1]
heat = influxquery("""
SELECT mean(power_w)
FROM "housebus.heating.heatmeter.1.0"
WHERE time >= now() - 10m
""")["results"][0]["series"][0]["values"][0][1]
flow_temp_heatmeter = influxquery("""
SELECT mean(flow_temp_k)
FROM "housebus.heating.heatmeter.1.0"
WHERE time >= now() - 10m
""")["results"][0]["series"][0]["values"][0][1]
print(outdoor_temp_10m, flow_temp_heatmeter)
cop = heat / electricity
cop_opt = optimum_cop(outdoor_temp_10m, max(30, flow_temp_heatmeter))
print('previous', round(previous_room_temp, 2), 'room temp', round(room_temp, 2), 'shift', shift, 'export', round(electric_export), 'compressor_freq', round(compressor_freq, 2), 'flow_temp', flow_temp, 'flow_target_temp', flow_target_temp)
print('cop, opt', round(cop, 2), round(cop_opt, 2))
room_temperature_target = interpolate(outdoor_temp_10m, -6, 10, 18.7, 20)
hour = datetime.now().hour
if hour >= 20:
print('night reduction active')
room_temperature_target = 18.5
elif hour <= 5:
print('deep night reduction')
room_temperature_target = 18
elif hour <= 6:
print('early morning')
room_temperature_target = 18.5
print('room_temperature_target', round(room_temperature_target, 4))
overheating_offset = interpolate(outdoor_temp_24h, 5, 15, 1.5, 0.5)
if compressor_freq >= 19:
if room_temp > room_temperature_target:
# we can move from Heat-only to Heat+DHW, because room temperature is probably reached
heishamon_EnableHeat()
if room_temp < room_temperature_target - 0.5:
# switch back to heat only until house is warmer again
heishamon_EnableHeatOnly()
# should we trigger DHW?
if dhw_temp < 48 and (room_temp > room_temperature_target or compressor_freq < 1):
#if dhw_temp < 30:
# # seems operation mode is not set to DHW+water
# heishamon_SetDemandControl(254)
# heishamon_SetForceDHW()
# sys.exit()
if dhw_temp < 42 and compressor_freq < 1 and room_temp < room_temperature_target - 1:
# both DHW and rooms are cold, let's do DHW before
heishamon_SetForceDHW()
sys.exit()
if dhw_temp <= 38 and electric_export > 3000 and all(x > 2000 for x in solar_forecast[:2]):
heishamon_SetDemandControl(254 if compressor_freq > 1 else 80)
heishamon_SetForceDHW()
if dhw_target_temp < 51:
heishamon_SetDHWTemp(51)
sys.exit()
# heat up before the sun is going down
if electric_export > interpolate(dhw_temp, 45, 40, 4000, 0) and all(x > 2000 for x in solar_forecast[:2]) and all(x < 2000 for x in solar_forecast[5:17]):
print('sun is shining but going down soon, trigger dhw!')
heishamon_SetDemandControl(254 if compressor_freq > 1 else 80)
heishamon_SetForceDHW()
if dhw_target_temp < 51:
heishamon_SetDHWTemp(51)
sys.exit()
if defrost > 0:
print('defrost was active. wait until water has heated up...')
sys.exit()
if compressor_freq < 1:
if room_temp < room_temperature_target - 1.5:
print('rooms really cold, try to enable heating...')
heishamon_SetDemandControl(80)
heishamon_EnableHeatOnly() # to prevent DHW from disturbing heat-up
heishamon_SetHeatRequestTemperature(interpolate(outdoor_temp_24h, 0, 10, 5, 0))
elif electric_export > interpolate(room_temperature_target - room_temp, 0, 2, 2000, 0) and room_temp < room_temperature_target:
# the colder it is in the house, the lower the solar export threshold will be until we turn on heating
print('rooms not warm enough and we have solar power, try to enable heating...')
heishamon_SetDemandControl(80)
heishamon_EnableHeat()
heishamon_SetHeatRequestTemperature(interpolate(outdoor_temp_24h, 0, 10, 5, 0))
sys.exit()
room_temp_diff = room_temp - previous_room_temp
if room_temp > room_temperature_target + overheating_offset + 2 and compressor_freq <= 20 and flow_temp > flow_target_temp:
heishamon_DisableHeat()
sys.exit()
new_shift = None
new_demand_control = compressor_current_to_demand_control(compressor_current)
#if cop < cop_opt - 0.1:
# print("bad CoP, increase demand control")
# new_demand_control += 25
if electric_export > 500:
if room_temp < room_temperature_target + overheating_offset and (room_temperature_target + overheating_offset - room_temp) / room_temp_diff > 1.5 :
if electric_export > 2000 or all(x >= solar_forecast[0] for x in solar_forecast[1:4]):
kick = round(interpolate(electric_export, 500, 4000, 1, 10))
new_shift = shift + kick
new_demand_control += 25
print('sun is shining! push up the temperature...')
if cop > cop_opt + 0.1 and kick < 3:
print('...but such good CoP! lets wait a bit longer')
sys.exit()
elif room_temp > room_temperature_target + overheating_offset + 1:
new_shift = shift - 1 + (flow_temp - flow_target_temp)
elif room_temp > room_temperature_target + overheating_offset and electric_export < -500 and all(x < 1300 for x in solar_forecast):
print('rooms really warm and no solar power, time for a heating pause')
heishamon_DisableHeat()
elif room_temp > room_temperature_target + 0.2:
if compressor_freq <= 20 and flow_temp >= flow_target_temp:
print('flow temp above flow target, compressor frequency low. risk of shutdown, do not reduce water target')
if room_temp > room_temperature_target + overheating_offset:
print('rooms really warm. time for a heating pause...')
heishamon_DisableHeat()
sys.exit()
if room_temp < room_temperature_target + 0.4 and room_temp_diff < -0.2:
print('room cooling down, do not reduce target further. quitting...')
sys.exit()
kick = 2 if room_temp > room_temperature_target + 0.5 else 1
new_shift = shift - kick + (flow_temp - flow_target_temp)
new_demand_control = 80
elif room_temp_diff > 0.1 and room_temp > room_temperature_target:
print('room target reached, but still rising')
new_shift = shift - 1 + (flow_temp - flow_target_temp)
elif room_temp_diff > 0.1 and (room_temperature_target - room_temp) / room_temp_diff < 1.5:
print('room is still heating up, no need for more power yet. quitting...')
sys.exit()
elif room_temp < room_temperature_target - 0.3 and flow_temp <= flow_target_temp - 2:
if flow_temp - flow_temp_before > 5:
print('water still heating up, do not change demand control yet')
sys.exit()
print('not enough power, increase demand control')
heishamon_SetDemandControl(compressor_current_to_demand_control(compressor_current) + 15)
sys.exit()
elif room_temp < room_temperature_target - 0.3:
kick = round(interpolate(room_temperature_target - room_temp, 0.3, 2, 1, 10))
print('heat up faster +', kick)
if cop > cop_opt + 0.1 and kick < 3:
print('...but such good CoP! lets wait a bit longer')
sys.exit()
new_shift = shift + kick + (flow_temp - flow_target_temp)
new_demand_control += 15
if new_shift:
new_shift = max(shift - 2, -5, min(5, new_shift))
if new_shift is not None and new_shift != shift:
heishamon_SetHeatRequestTemperature(new_shift)
heishamon_SetDemandControl(new_demand_control)