400 lines
15 KiB
Python
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)
|