|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""Controlling a Daikin Altherma 3 heatpump, and presenting data in Home Assistant. |
| 3 | +
|
| 4 | +This app is used to control a Daikin heatpump, by reading the temperatures |
| 5 | +of deposit for the underfloor heating. |
| 6 | +
|
| 7 | +Based on the temperatures and the time of the day (electricity periods P1, P2 and P3) it |
| 8 | +is determined to start/stop the heat pump (Daikin Altherma3) |
| 9 | +
|
| 10 | +The data is made available in modbus and observed by a Home Assistant server, and presented |
| 11 | +to the home users. |
| 12 | +
|
| 13 | +Schematic of deposit: |
| 14 | +
|
| 15 | + +--------+ |
| 16 | + A) --> -+ | F) |
| 17 | + | E) +--+- --> C) |
| 18 | + | | | |
| 19 | + B) <-- -+ | | |
| 20 | + | +--+- <-- D) |
| 21 | + +--------+ |
| 22 | +
|
| 23 | + A) Intake from heat pump (hot) |
| 24 | + B) Return to heat pump (cold) |
| 25 | + C) Output to underfloor circulation pump (hot) |
| 26 | + D) return from underfloor pipes (cold) |
| 27 | + E) Output from tank (hot) |
| 28 | + F) Thermostatic mixing valve (output is fixed at max 30 degrees) |
| 29 | +
|
| 30 | +The 5 point are measured. |
| 31 | +
|
| 32 | +The algorithm is quite simple: |
| 33 | +
|
| 34 | + Difference between A) and heat pump setpoint shows the loss in the connection pipes. |
| 35 | + Difference between D) and underfloor heating thermostats show if heat is required. |
| 36 | +
|
| 37 | +Remark: the return from the underfloor heating is connected to a thermostatic valve, |
| 38 | +on the output to the circulation pump. This allows to e.g. heat the tank to 50 degrees, |
| 39 | +while still circulating 30 degrees. Using the thermostatic valve, dramatically reduces |
| 40 | +the number of times the heat pump is started (when started it runs longer) and thus saving |
| 41 | +electricity. |
| 42 | +
|
| 43 | +The thermo meters are read via 1-wire protocol. |
| 44 | +
|
| 45 | +usage:: |
| 46 | +
|
| 47 | + heatpump.py [-h] |
| 48 | + [--log {critical,error,warning,info,debug}] |
| 49 | + [--port <PORT>] |
| 50 | +
|
| 51 | + -h, --help |
| 52 | + show this help message and exit |
| 53 | + -l, --log {critical,error,warning,info,debug} |
| 54 | + set log level, default is info |
| 55 | + -p, --port PORT |
| 56 | + set port to listen on |
| 57 | +""" |
| 58 | +import argparse |
| 59 | +import asyncio |
| 60 | + |
| 61 | +from pymodbus import Log, pymodbus_apply_logging_config |
| 62 | +from pymodbus.server import ModbusTcpServer |
| 63 | +from pymodbus.simulator import DataType, SimData, SimDevice |
| 64 | +from pymodbus.constants import ExcCodes |
| 65 | + |
| 66 | +DEVICE_ID = 1 |
| 67 | + |
| 68 | +def get_commandline(cmdline: list[str] | None = None) -> argparse.Namespace: |
| 69 | + """Read and check command line arguments.""" |
| 70 | + parser = argparse.ArgumentParser(description="server_update") |
| 71 | + parser.add_argument( |
| 72 | + "-l", |
| 73 | + "--log", |
| 74 | + choices=["critical", "error", "warning", "info", "debug"], |
| 75 | + help="set log level, default is info", |
| 76 | + dest="log", |
| 77 | + default="info", |
| 78 | + type=str, |
| 79 | + ) |
| 80 | + parser.add_argument( |
| 81 | + "-p", |
| 82 | + "--port", |
| 83 | + help="set listen port, default is 5020", |
| 84 | + dest="port", |
| 85 | + default=5020, |
| 86 | + type=str, |
| 87 | + ) |
| 88 | + args = parser.parse_args(cmdline) |
| 89 | + pymodbus_apply_logging_config(args.log.upper()) |
| 90 | + return args |
| 91 | + |
| 92 | + |
| 93 | +def setup_updating_server(cmdline=None): |
| 94 | + """Run server setup.""" |
| 95 | + args = get_commandline(cmdline=cmdline) |
| 96 | + |
| 97 | + device = SimDevice(DEVICE_ID, simdata=[ |
| 98 | + # bit 0, Heat pump requested active |
| 99 | + # bit 1, Alive signal (toggles every minute) |
| 100 | + # bit 2, Call signal (toggles with every call) |
| 101 | + # bit 3, Heat pump set to be active |
| 102 | + SimData(0, DataType.BITS, readonly=True), |
| 103 | + # 4 thermo meters A,B,C,D |
| 104 | + SimData(1, count=4, datatype=DataType.FLOAT32, readonly=True), |
| 105 | + # Setpoint to activate heat pump (output to underfloor pipes) |
| 106 | + # Setpoint to run heat pump at night (return to heat pump) |
| 107 | + SimData(9, count=2, datatype=DataType.FLOAT32), |
| 108 | + ]) |
| 109 | + server = ModbusTcpServer( |
| 110 | + device, |
| 111 | + address=("", args.port), |
| 112 | + context=device, |
| 113 | + ) |
| 114 | + return server |
| 115 | + |
| 116 | + |
| 117 | +async def updating_task(server): |
| 118 | + """Update values in server. |
| 119 | +
|
| 120 | + This task runs continuously beside the server |
| 121 | + It will increment some values each second. |
| 122 | + """ |
| 123 | + func_code = 3 |
| 124 | + device_id = 0x01 |
| 125 | + address = 0x10 |
| 126 | + count = 6 |
| 127 | + |
| 128 | + # set values to zero |
| 129 | + values = await server.async_getValues(device_id, func_code, address, count=count) |
| 130 | + values = [0 for v in values] |
| 131 | + await server.async_setValues(device_id, func_code, address, values) |
| 132 | + |
| 133 | + txt = ( |
| 134 | + f"updating_task: started: initialised values: {values!s} at address {address!s}" |
| 135 | + ) |
| 136 | + print(txt) |
| 137 | + Log.debug(txt) |
| 138 | + |
| 139 | + # incrementing loop |
| 140 | + while True: |
| 141 | + await asyncio.sleep(1) |
| 142 | + |
| 143 | + values = await server.async_getValues(device_id, func_code, address, count=count) |
| 144 | + values = [v + 1 for v in values] |
| 145 | + await server.async_setValues(device_id, func_code, address, values) |
| 146 | + |
| 147 | + txt = f"updating_task: incremented values: {values!s} at address {address!s}" |
| 148 | + print(txt) |
| 149 | + Log.debug(txt) |
| 150 | + |
| 151 | +async def catch_requests( |
| 152 | + function_code: int, |
| 153 | + start_address: int, |
| 154 | + address: int, |
| 155 | + count: int, |
| 156 | + current_registers: list[int], |
| 157 | + set_values: list[int] | list[bool] | None |
| 158 | + ) -> : |
| 159 | + """Run action.""" |
| 160 | + |
| 161 | + |
| 162 | +async def run_updating_server(server): |
| 163 | + """Start updating_task concurrently with the current task.""" |
| 164 | + task = asyncio.create_task(updating_task(server)) |
| 165 | + task.set_name("example updating task") |
| 166 | + await server.serve_forever() # start the server |
| 167 | + task.cancel() |
| 168 | + |
| 169 | + |
| 170 | +async def main(cmdline=None): |
| 171 | + """Combine setup and run.""" |
| 172 | + server = setup_updating_server(cmdline=cmdline) |
| 173 | + await run_updating_server(server) |
| 174 | + |
| 175 | + |
| 176 | +if __name__ == "__main__": |
| 177 | + asyncio.run(main(), debug=True) |
0 commit comments