├── LICENSE ├── Payload-data-read.md ├── Payload-data-write.md ├── README.md ├── VIU_Daikin Altherma.xml ├── VO_Daikin Altherma LT.xml ├── arduino-altherma-controller ├── 01-interfaces.ino ├── 02-UDP.ino ├── 03-P1P2.ino ├── 04-webserver.ino ├── 05-pages.ino ├── advanced_settings.h ├── arduino-altherma-controller.ino └── src │ └── P1P2Serial_mod │ ├── P1P2Serial_ADC.h │ ├── P1P2Serial_mod.cpp │ └── P1P2Serial_mod.h └── pics ├── HW.jpg ├── daikin1.png ├── daikin2.png ├── daikin3.png ├── daikin4.png ├── daikin5.png ├── daikin6.png ├── daikin7.png ├── loxone.mp4 ├── loxone1.png ├── loxone10.png ├── loxone2.png ├── loxone3.png ├── loxone4.png ├── loxone5.png ├── loxone6.png ├── loxone7.png ├── loxone8.png ├── loxone9.png └── reset_bridges_Ethernet.jpg /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /Payload-data-read.md: -------------------------------------------------------------------------------- 1 | ## Read data from P1P2 bus 2 | 3 | Listening to the P1/P2 bus should be quite safe. But still, be aware that the bus is powered (15V DC), so avoid short circuits. P1P2 bus uses a master/slave protocol. Communication is asynchronous: master sends a "request" data packet and waits for "response" data packet from a slave (or timeout) before sending new packet. There can only be one master. The master is the main controller (Daikin user interface attached to the unit), all other devices on the bus act as slaves: the heat pump itself and external controllers. Each data packet consists of: 4 | 5 | * **Header** 6 | * 1 byte indicating **direction of the communication**: request from master (0x00), response from slave (0x40) or other (0x80) 7 | * 1 byte **slave address**: 0x00 heat pump 8 | * 1 byte **packet type**: the packet type indicates what kind of data is transmitted in the payload. For example, packet type 0x11 (response from the heat pump) contains temperature values (leaving and returning water, outdoor and indoor temperature etc.) 9 | * **Payload** 10 | * Up to cca 30 bytes of **payload data**. Payload length and content are determined by the header. For example, packet with header 0x400011 (= response from heat pump, packet type 0x11) always has 18 byte payload. First and second bytes of the payload in this particular packet always hold the leaving water temperature. Content (and length) of thepayload may vary depending on the heat pump model. 11 | * **Checksum** 12 | * 1 byte **CRC checksum** 13 | 14 | There is a large number of packet types observed on the P1P2 bus, each of them with specific payload. For more details about the P1P2 protocol (what we learned so far through reverse engineering) and for a full overview of the packet types identified (empirically observed on the tested heat pumps), please visit https://github.com/Arnold-n/P1P2Serial. 15 | 16 | Data packet is forwarded as-it-is (Header + Payload) as raw HEX. 17 | 18 | ## Daikin Altherma Hybrid and Daikin Altherma LT protocol data format 19 | 20 | Daikin P1P2 protocol payload data format for Daikin Altherma Hybrid (perhaps all EHYHB(H/X) models) and Daikin Altherma LT (perhaps all EHV(H/X) models). Big thanks go to Arnold Niessen for his development of the P1P2 adapter and the P1P2Serial library. Most of the information in these tables is based on his observation of the P1P2 protocol. This document is based on reverse engineering and assumptions, so there may be mistakes and misunderstandings. 21 | 22 | This document describes payload data of few **selected readable packet types** (packet types 0x10 - 0x16 and 0xB8) exchanged between the **main controller (requests)** and the **heat pump (responses)**. For a more complete overview see Arnold's documentation here: https://github.com/Arnold-n/P1P2Serial/tree/master/doc 23 | 24 | ### Data types 25 | 26 | The following data-types were observed or suspected in the payload data: 27 | 28 | | Data type | Definition | 29 | |---------------|:-------------------------------------| 30 | | flag8 | byte composed of 8 single-bit flags | 31 | | c8 | ASCII character byte | 32 | | s8 | signed 8-bit integer -128 .. 127 | 33 | | u8 | unsigned 8-bit integer 0 .. 255 | 34 | | u6 | unsigned 6-bit integer 0 .. 63 | 35 | | u2 | unsigned 2-bit integer 0 .. 3 or 00 .. 11 | 36 | | s16 | signed 16-bit integer -32768..32767 | 37 | | u16 | unsigned 16-bit integer 0 .. 65535 | 38 | | u24 | unsigned 24-bit integer | 39 | | u32 | unsigned 32-bit integer | 40 | | u16hex | unsigned 16-bit integer output as hex 0x0000-0xFFFF | 41 | | u24hex | unsigned 24-bit integer | 42 | | u32hex | unsigned 32-bit integer | 43 | | f8.8 (code: f8_8) | signed fixed point value : 1 sign bit, 7 integer bit, 8 fractional bits (two’s compliment, see explanation below) | 44 | | f8/8 (code: f8s8) | Daikin-style fixed point value: 1st byte is value before comma, and 2nd byte is first digit after comma (see explanation below) | 45 | | s-abs4 | Daikin-style temperature deviation: bit 4 is sign bit, bits 0-3 is absolute value of deviation | 46 | | sfp7 | signed floating point value: 1 sign bit, 4 mantissa bits, 2 exponent bits (used for field settings) | 47 | | u8div10 | unsigned 8-bit integer 0 .. 255, to be divided by 10 | 48 | | u16div10 | unsigned 16-bit integer 0 .. 65535, to be divided by 10 | 49 | | t8 | schedule moment in 10 minute increments from midnight (0=0:00, 143=23:50) | 50 | | d24 | day in format YY MM DD | 51 | 52 | Explanation of f8.8 format: a temperature of 21.5°C in f8.8 format is represented by the 2-byte value in 1/256th of a unit as 1580 hex (1580hex = 5504dec, dividing by 256 gives 21.5). A temperature of -5.25°C in f8.8 format is represented by the 2-byte value FAC0 hex (FAC0hex = - (10000hex-FACOhex) = - 0540hex = - 1344dec, dividing by 256 gives -5.25). 53 | 54 | Explanation of f8/8 format: a temperature of 21.5°C in f8/8 format is represented by the byte value of 21 (0x15) followed by the byte value of 5 (0x05). So far this format was only detected for the setpoint temperature, which is usually not negative, so we don't know how negative numbers are stored in this format. 55 | 56 | # Packet types 10-1F form communication package between main controller and heat pump 57 | 58 | Packet types 10-16 are part of the regular communication pattern between main controller and heat pump. 59 | 60 | ## Packet type 10 - operating status 61 | 62 | ### Packet type 10: request 63 | 64 | Header: 000010 65 | 66 | |Byte(:bit)| Hex value observed | Description | Data type 67 | |:---------|:-------------------|:-------------------------|:- 68 | |0:0 | 0/1 | Heat pump (off/on) | bit 69 | |0:other | 0 | ? | bit 70 | |1:7 | 0/1 | Heat pump (off/on) | bit 71 | |1:0 | 0/1 | 1=Heating mode | bit 72 | |1:1 | 0/1 | 1=Cooling mode | bit 73 | |1:0 | 1 | Operating mode gas? | bit 74 | |1:other | 0 | Operating mode? | bit 75 | |2:1 | 0/1 | DHW tank power (off/on) | bit 76 | |2:0 | 0/1 | DHW (off/on) | bit 77 | |2:other | 0 | ? | bit 78 | | 3 | 00 | ? | 79 | | 4 | 00 | ? | 80 | | 5 | 00 | ? | 81 | | 6 | 00 | ? | 82 | | 7-8 | 13 05 | Target room temperature | f8.8 83 | | 9:6 | 0/1 | ? | bit 84 | | 9:5 | 0/1 | Heating/Cooling automatic mode | bit 85 | | 9:others | 0 | ? | bit 86 | |10:2 | 0/1 | Quiet mode (off/on) | bit 87 | |10:others | 0 | ? | bit 88 | | 11 | 00 | ? | 89 | |12:3 | 1 | ? | bit 90 | |12:others | 0 | ? | bit 91 | | 13 | 00 | ? | 92 | | 14 | 00 | ? | 93 | | 15 | 0F | ? | flag8/bits? 94 | | 16 | 00 | ? | 95 | |17:6 | 0/1 | operation (off/on) | bit 96 | |17:1 | 0/1 | booster (off/on) | bit 97 | |17:others | 0 | ? | bit 98 | | 18 | 3C | DHW target temperature | u8 / f8.8? 99 | | 19 | 00 | fractional part byte 18? | 100 | 101 | ### Packet type 10: response 102 | 103 | Header: 400010 104 | 105 | |Byte(:bit)| Hex value observed | Description | Data type 106 | |:---------|:-------------------|:-------------------------|:- 107 | | 0:0 | 0/1 | Heating power (off/on) | bit 108 | | 0:other | 0 | ? | bit 109 | | 1:7 | 0/1 | Operating mode gas? | bit 110 | | 1:0 | 0 | Operating mode? | bit 111 | | 1:other | 0 | Operating mode? | bit 112 | | 2:7 | 0/1 | DHW tank power (off/on) | bit 113 | | 2:6 | 0/1 | Additional zone (off/on) | bit 114 | | 2:5 | 0/1 | Main zone (off/on) | bit 115 | | 2:4 | 0/1 | ? | bit 116 | | 2:3 | 0 | ? | bit 117 | | 2:2 | 0 | ? | bit 118 | | 2:1 | 0/1 | cooling (off/on) | bit 119 | | 2:0 | 0/1 | heating (off/on) | bit 120 | | 3:4 | 0/1 | **DHW boost (off/on)** | bit 121 | | 3:0 | 0/1 | **DHW (off/on)** | bit 122 | | 3:others | 0 | ? | bit 123 | | 4 | 3C | DHW target temperature| u8 / f8.8? 124 | | 5 | 00 | +fractional part? | 125 | | 6 | 0F | ? | u8 / flag8? 126 | | 7 | 00 | ? 127 | | 8 | 14 | Target room temperature | u8 / f8.8? 128 | | 9 | 00 | ? 129 | |10 | 1A | ? 130 | |11:2 | 0/1 | Quiet mode (off/on) | bit 131 | |11:1 | 0/1 | **?? (end of disinfection)** | bit 132 | |11:0 | 0/1 | **Disinfection mode (off/on)** | bit 133 | |11 | 0 | ? | bit 134 | |12 | 00 | Error code part 1 | u8 135 | |13 | 00 | Error code part 2 | u8 136 | |14 | 00 | Error subcode | u8 137 | |15-17 | 00 | ? 138 | |17:1 | 0/1 | Defrost operation | bit 139 | |18:3 | 0/1 | Circ.pump (off/on) | bit 140 | |18:1 | 0/1 | **Backup Heater step 1 DHW (off/on)** | bit 141 | |18:0 | 0/1 | Compressor (off/on) | bit 142 | |18:other | 0 | ? | bit 143 | |19:2 | 0/1 | DHW mode | bit 144 | |19:1 | 0/1 | gasboiler active1 (off/on) | bit 145 | |19:other | 0 | ? | bit 146 | 147 | Error codes: tbd, HJ-11 is coded as 024D2C, 89-2 is coded as 08B908, 89-3 is coded as 08B90C. 148 | 149 | ## Packet type 11 - temperatures 150 | 151 | ### Packet type 11: request 152 | 153 | Header: 000011 154 | 155 | | Byte nr | Hex value observed | Description | Data type 156 | |:--------|:-------------------|:------------------------|:- 157 | | 0-1 | XX YY | Actual room temperature | f8.8 158 | | 2 | 00 | ? 159 | | 3 | 00 | ? 160 | | 4 | 00 | ? 161 | | 5 | 00 | ? 162 | | 6 | 00 | ? 163 | | 7 | 00 | ? 164 | 165 | ### Packet type 11: response 166 | 167 | Header: 400011 168 | 169 | | Byte nr | Hex value observed | Description | Data type 170 | |:--------|:-------------------|:------------------------------------------------------|:- 171 | | 0-1 | XX YY | LWT temperature | f8.8 172 | | 2-3 | XX YY | DHW temperature tank (if present) | f8.8 173 | | 4-5 | XX YY | Outside temperature (raw; in 0.5 degree resolution) | f8.8 174 | | 6-7 | XX YY | RWT | f8.8 175 | | 8-9 | XX YY | Mid-way temperature heat pump - gas boiler | f8.8 176 | | 10-11 | XX YY | Refrigerant temperature | f8.8 177 | | 12-13 | XX YY | Actual room temperature | f8.8 178 | | 14-15 | XX YY | External outside temperature sensor (if connected); otherwise Outside temperature 2 derived from external unit sensor, but stabilized; does not change during defrosts; perhaps averaged over time | f8.8 179 | | 16-19 | 00 | ? 180 | 181 | ## Packet type 12 - Time, date and status flags 182 | 183 | ### Packet type 12: request 184 | 185 | Header: 000012 186 | 187 | | Byte(:bit) | Hex value observed | Description | Data type 188 | |:-----------|:------------------------------|:---------------------------------|:- 189 | | 0:1 | 0/1 | pulse at start each new hour | bit 190 | | 0:other| 0 | ? | bit 191 | | 1 | 00-06 | day of week (0=Monday, 6=Sunday) | u8 192 | | 2 | 00-17 | time - hours | u8 193 | | 3 | 00-3B | time - minutes | u8 194 | | 4 | 13-16 | date - year (16 = 2022) | u8 195 | | 5 | 01-0C | date - month | u8 196 | | 6 | 01-1F | date - day of month | u8 197 | | 7-11 | 00 | ? | 198 | | 12:6 | 0/1 | restart process indicator ? | bit 199 | | 12:5 | 0/1 | restart process indicator ? | bit 200 | | 12:0 | 0/1 | restart process indicator ? | bit 201 | | 12:other| 0 | ? | bit 202 | | 13:2 | 0/1 | once 0, then 1 | bit 203 | | 13:other| 0 | ? | bit 204 | | 14 | 00 | ? | 205 | 206 | Byte 12 has following pattern upon restart: 1x 00; 1x 01; then 41. A single value of 61 triggers an immediate heat pump restart. 207 | 208 | ### Packet type 12: response 209 | 210 | Header: 400012 211 | 212 | | Byte(:bit) | Hex value observed | Description | Data type 213 | |:-----------|:-------------------|:----------------------|:- 214 | | 0 | 40 | ? 215 | | 1 | 40 | ? 216 | | 2-9 | 00 | ? 217 | | 10:4 | 0/1 | kWh preference input | bit 218 | | 10:other| 0 | | bit 219 | | 11 | 00/7F | once 00, then 7F | u8 220 | | 12 | | operating mode | flag8 221 | | 12:7 | 0/1 | DHW active2 | bit 222 | | 12:6 | 0/1 | gas? (depends on DHW on/off and heating on/off) | bit 223 | | 12:0 | 0/1 | heat pump? | bit 224 | | 12:other| 0 | ? | bit 225 | | 13:2 | 1 | ? | bit 226 | | 13:other| 0 | ? | bit 227 | | 14-19 | 00 | ? 228 | 229 | ## Packet type 13 - software version, DHW target temperature and flow 230 | 231 | ### Packet type 13: request 232 | 233 | Header: 000013 234 | 235 | | Byte(:bit) | Hex value observed | Description | Data type 236 | |:-----------|:-------------------|:-----------------------------------|:- 237 | | 0-1 | 00 | ? 238 | | 2:4 | 0/1 | ? | bit 239 | | 2:5,3-0 | 0 | ? | bit 240 | | 2:7-6 | 00/01/10/11 | modus ABS / WD / ABS+prog / WD+dev | bit2 241 | 242 | ### Packet type 13: response 243 | 244 | Header: 400013 245 | 246 | | Byte(:bit) | Hex value observed | Description | Data type 247 | |:-----------|:-------------------|:----------------------------|:- 248 | | 0 | 3C | DHW target temperature (one packet delayed/from/via boiler?/ 0 in first packet after restart) | u8 / f8.8 ? 249 | | 1 | 00 | +fractional part? 250 | | 2 | 01 | ? 251 | | 3 | 40/D0 | ? 252 | | 3:5-0 | 0 | ? | bit 253 | | 3:7-6 | 00/01/10/11 | modus ABS / WD / ABS+prog / WD+dev | bit2 254 | | 4-7 | 00 | ? 255 | | 8-9 | **FFFC/FFFD/**0000-010E | flow (in 0.1 l/min) (EHV/EHYHB only. Zero on EJHA) | **s16div10 (negative if pump stops, probably bad calibration)** 256 | | 10-11 | xxxx | software version inner unit | u16 257 | | 12-13 | xxxx | software version outer unit | u16 258 | |EHV only: 14| 00 | ? | u8 259 | |EHV only: 15| 00 | ? | u8 260 | 261 | The DHW target temperature is one packet delayed - perhaps this is due to communication with the Intergas gas boiler and confirms the actual gas boiler setting? 262 | 263 | ## Packet type 14 - LWT target temperatures, temperature deviation, .. 264 | 265 | ### Packet type 14: request 266 | 267 | Header: 000014 268 | 269 | | Byte(:bit) | Hex value observed | Description | Data type 270 | |:-----------|:-------------------|:---------------------------------|:- 271 | | 0-1 | 27 00 | LWT setpoint Heating Main zone | f8.8 or f8/8? 272 | | 2-3 | 12 00 | LWT setpoint Cooling Main zone | f8.8 or f8/8? 273 | | 4-5 | 27 00 | LWT setpoint Heating Add zone | f8.8 or f8/8? 274 | | 6-7 | 12 00 | LWT setpoint Cooling Add zone | f8.8 or f8/8? 275 | | 8 | 00-0A,10-1A | LWT deviation Heating Main zone | s-abs4 276 | | 9 | 00-0A,10-1A | LWT deviation Cooling Main zone | s-abs4 277 | | 10 | 00-0A,10-1A | LWT deviation Heating Add zone | s-abs4 278 | | 11 | 00-0A,10-1A | LWT deviation Cooling Add zone | s-abs4 279 | | 12 | 00/37 | first package 37 instead of 00 | u8 280 | | 13-14 | 00 | ? 281 | 282 | ### Packet type 14: response 283 | 284 | Header: 400014 285 | 286 | | Byte(:bit) | Hex value observed | Description | Data type 287 | |:-----------|:-------------------|:--------------------------|:- 288 | | 0-14 | XX | echo of 000014-{00-14} | 289 | | 15-16 | 1C-24 00-09 | Target LWT Main zone in 0.1 degree (based on outside temperature in 0.5 degree resolution)| f8/8? 290 | | 17-18 | 1C-24 00-09 | Target LWT Add zone in 0.1 degree (based on outside temperature in 0.5 degree resolution)| f8/8? 291 | 292 | ## Packet type 15 - temperatures, operating mode 293 | 294 | ### Packet type 15: request 295 | 296 | Header: 000015 297 | 298 | | Byte(:bit) | Hex value observed | Description | Data type 299 | |:-----------|:----------------------|:--------------------------------|:- 300 | | 0 | 00 | ? 301 | | 1-2 | 01/09/0A/0B 54/F4/C4/D6/F0 | schedule-induced operating mode? | flag8,flag8? 302 | | 3 | 00 | ? 303 | | 4 | 03 | ? 304 | | 5 | 20/52 | ? 305 | 306 | ### Packet type 15: response 307 | 308 | Header: 400015 309 | 310 | | Byte(:bit) | Hex value observed | Description | Data type 311 | |:-----------|:----------------------|:----------------------------------|:- 312 | | 0-1 | 00 | Refrigerant temperature? | f8.8? 313 | | 2-3 | FD-FF,00-08 00/80 | Refrigerant temperature (in 0.5C) | f8.8 314 | | 4-5 | 00 | Refrigerant temperature? | f8.8? 315 | |EHV only: 6 | 00-19 | parameter number | u8/u16 316 | |EHV only: 7 | 00 | (part of parameter or value?) | 317 | |EHV only: 8 | XX | ? | s16div10_LE? 318 | 319 | EHV is the only model for which we have seen use of the mechanism in the basic packet types. 320 | The following parameters have been observed in this packet type on EHV model only: 321 | 322 | | Parameter number | Parameter Value | Description 323 | |:-----------------|:------------------------------|:- 324 | |EHV: 05 | 96 | 15.0 degree? 325 | |EHV: 08 | 73,78,7D | 11.5, 12.0, 12.5 ? 326 | |EHV: 0A | C3,C8 | 19.5, 20.0 327 | |EHV: 0C | 6E,73,78 | 11.0, 11.5, 12.0 328 | |EHV: 0E | B9,BE | 18.5, 19.0 329 | |EHV: 0F | 68,6B | 10.4, 10.7 330 | |EHV: 10 | 05 | 0.5 331 | |EHV: 19 | 01 | 0.1 332 | |EHV: others | 00 | ? 333 | 334 | These parameters are likely s16div10. 335 | 336 | ## Packet type 16 - temperatures 337 | 338 | Only observed on EHV\* and EJHA\* heat pumps. 339 | 340 | ### Packet type 16: request 341 | 342 | Header: 000016 343 | 344 | | Byte(:bit) | Hex value observed | Description | Data type 345 | |:-----------|:-------------------|:------------------|:- 346 | | 0-1 | 00 | ? 347 | | 2-3 | 32 14 | room temperature? | f8.8? 348 | | 4-15 | 00 | ? 349 | 350 | ### Packet type 16: response 351 | 352 | Header: 400016 353 | 354 | | Byte(:bit) | Hex value observed | Description | Data type 355 | |:-----------|:-------------------|:---------------------------------|:- 356 | | 0 | | **Current in 0.1 A** | u8div10 357 | | 1 | | **Power input in 0.1 kW** | u8div10 358 | | 0-7 | 00 | ? | 359 | | 6 | | **Heating/cooling output in 0.1 kW** | u8div10 360 | | 7 | | **DHW output in 0.1 kW** | u8div10 361 | | 8 | E6 | ? | 362 | 363 | # Packet types A1 and B1 communicate text data 364 | 365 | ## Packet type A1 366 | 367 | Used for name of product? 368 | 369 | ### Packet type A1: request 370 | 371 | Header: 0000A1 372 | 373 | | Byte nr | Hex value observed | Description | Data type 374 | |:--------------|:------------------------------|:----------------------|:- 375 | | 0 | 00 | ? 376 | | 1-15 | 30 | ASCII '0' | c8 377 | | 16-17 | 00 | ASCII '\0' | c8 378 | 379 | ### Packet type A1: response 380 | 381 | Header: 4000A1 382 | 383 | | Byte nr | Hex value observed | Description | Data type 384 | |:--------------|:------------------------------|:----------------------|:- 385 | | 0 | 00 | ? 386 | | 1-15 | 00 | ASCII '\0' (missing) name outside unit | c8 387 | | 16-17 | 00 | ASCII '\0' | c8 388 | 389 | ## Packet type B1 - heat pump name 390 | 391 | Product name. 392 | 393 | ### Packet type B1: request 394 | 395 | Header: 0000B1 396 | 397 | | Byte nr | Hex value observed | Description | Data type 398 | |:--------------|:------------------------------|:----------------------|:- 399 | | 0 | 00 | ? 400 | | 1-15 | 30 | ASCII '0' | c8 401 | | 16-17 | 00 | ASCII '\0' | c8 402 | 403 | ### Packet type B1: response 404 | 405 | Header: 4000B1 406 | 407 | | Byte nr | Hex value observed | Description | Data type 408 | |:--------------|:------------------------------|:----------------------|:- 409 | | 0 | 00 | ? 410 | | 1-12 | XX | ASCII "EHYHBH08AAV3" name inside unit | c8 411 | | 13-17 | 00 | ASCII '\0' | c8 412 | 413 | # Packet type B8 - counters, #hours, #starts, electricity used, energy produced 414 | 415 | Counters for energy consumed and operating hours. The main controller specifies which data type it would like to receive. The heat pump responds with the requested data type counters. A B8 package is only transmitted by the main controller after a manual menu request for these counters. P1P2Monitor can insert B8 requests to poll these counters, but this violates the rule that an auxiliary controller should not act as main controller. But if timed carefully, it works. 416 | 417 | ### Packet type B8: request 418 | 419 | Header: 0000B8 420 | 421 | | Byte nr | Hex value observed | Description | Data type 422 | |:--------------|:------------------------------|:----------------------|:- 423 | | 0 | XX | data type requested
00: energy consumed
01: energy produced
02: pump and compressor hours
03: backup heater hours
04: compressor starts
05: boiler hours and starts | u8 424 | 425 | ### Packet type B8: response 426 | 427 | Header: 4000B8 428 | 429 | #### Data type 00 430 | 431 | | Byte nr | Hex value observed | Description | Data type 432 | |:--------------|:------------------------------|:--------------------------------------|:- 433 | | 0 | 00 | data type 00 : energy consumed (kWh) | u8 434 | | 1-3 | XX XX XX | by backup heater for heating | u24 435 | | 4-6 | XX XX XX | by backup heater for DHW | u24 436 | | 7-9 | 00 XX XX | by compressor for heating | u24 437 | | 10-12 | XX XX XX | by compressor for cooling | u24 438 | | 13-15 | XX XX XX | by compressor for DHW | u24 439 | | 16-18 | XX XX XX | total | u24 440 | 441 | #### Data type 01 442 | 443 | | Byte nr | Hex value observed | Description | Data type 444 | |:--------------|:------------------------------|:-------------------------------------|:- 445 | | 0 | 01 | data type 01 : energy produced (kWh) | u8 446 | | 1-3 | XX XX XX | for heating | u24 447 | | 4-6 | XX XX XX | for cooling | u24 448 | | 7-9 | XX XX XX | for DHW | u24 449 | | 10-12 | XX XX XX | total | u24 450 | 451 | On EJHA\* all counters for type 01 are always zero. 452 | 453 | #### Data type 02 454 | 455 | | Byte nr | Hex value observed | Description | Data type 456 | |:--------------|:------------------------------|:-------------------------------|:- 457 | | 0 | 02 | data type 02 : operating hours | u8 458 | | 1-3 | XX XX XX | pump hours | u24 459 | | 4-6 | XX XX XX | compressor for heating | u24 460 | | 7-9 | XX XX XX | compressor for cooling | u24 461 | | 10-12 | XX XX XX | compressor for DHW | u24 462 | 463 | #### Data type 03 464 | 465 | | Byte nr | Hex value observed | Description | Data type 466 | |:--------------|:------------------------------|:-------------------------------|:- 467 | | 0 | 03 | data type 03 : operating hours | u8 468 | | 1-3 | XX XX XX | backup heater1 for heating | u24 469 | | 4-6 | XX XX XX | backup heater1 for DHW | u24 470 | | 7-9 | XX XX XX | backup heater2 for heating | u24 471 | | 10-12 | XX XX XX | backup heater2 for DHW | u24 472 | | 13-15 | XX XX XX | ? | u24 473 | | 17-18 | XX XX XX | ? | u24 474 | 475 | #### Data type 04 476 | 477 | | Byte nr | Hex value observed | Description | Data type 478 | |:--------------|:------------------------------|:----------------------------|:- 479 | | 0 | 04 | data type 04 | u8 480 | | 1-3 | XX XX XX | ? | u24 481 | | 4-6 | XX XX XX | ? | u24 482 | | 7-9 | XX XX XX | ? | u24 483 | | 10-12 | XX XX XX | number of compressor starts | u24 484 | 485 | #### Data type 05 486 | 487 | | Byte nr | Hex value observed | Description | Data type 488 | |:--------------|:------------------------------|:------------------------------------------|:- 489 | | 0 | 05 | data type 05 : gas boiler in hybrid model | u8 490 | | 1-3 | XX XX XX | boiler operating hours for heating | u24 491 | | 4-6 | XX XX XX | boiler operating hours for DHW | u24 492 | | 7-9 | XX XX XX | gas usage for heating (unit tbd) | u24 493 | | 10-12 | XX XX XX | gas usage for heating (unit tbd) | u24 494 | | 13-15 | XX XX XX | number of boiler starts | u24 495 | | 16-18 | XX XX XX | gas usage total (unit tbd) | u24 496 | 497 | Internal gas metering seems only supported on newer models, not on the AAV3. 498 | -------------------------------------------------------------------------------- /Payload-data-write.md: -------------------------------------------------------------------------------- 1 | ## Write data to P1P2 bus 2 | 3 | Arduino adapter communicates with the main controller (user interface), rather than the heat pump itself. Also, the program goes to great lengths to avoid bus collision with packets sent by other devices on the bus (by the heat pump, main controller, other external controllers). Yet be aware: hic sunt leones and you are on your own. **No guarantees, use this program at your own risk.** Remember that you can damage or destroy your (expensive) heat pump. Be careful and watch for errors in the web interface. I also recommend reading documentation provided by the author of the library: https://github.com/Arnold-n/P1P2Serial 4 | 5 | Arduino acts as an external controller. It waits for a handshake packet (type 0x30) from the main Daikin controller (= user interface attached at the indoor unit), replies with response (packet type 0x30), and in the next round of request-response sends new data (commands) to the main controller. 6 | 7 | These writeable packets have a specific structure: 8 | 9 | * **Header** 10 | * 1 byte indicating **direction of the communication**: request from master (0x00), response from slave (0x40) 11 | * 1 byte **slave address**: 0xF0, 0xF1 external controller(s) 12 | * 1 byte **packet type**: the packet type indicates what kind of data is transmitted in the payload 13 | * **Payload** 14 | * 2 bytes (little endian) **parameter number** 15 | * 1 or 2 bytes (little endian) **parameter value** 16 | * more parameter number-value pairs can be sent until the payload is full 17 | * payload has a fixed length, so 0xFF is used to fill the empty space 18 | * **Checksum** 19 | * 1 byte **CRC checksum** 20 | 21 | The program forms the packet itself, all you need to do is send the command (via UDP) using this format: 22 | 23 | `` 24 | 25 | Remember that both parameter number and value use **little endian bytes order**. 26 | 27 | Here are few examples of commands you can send via UDP or Serial: 28 | 29 | `35400001` = turn DHW on
30 | `35`: packet type 0x35
31 | `4000`: parameter number 40
32 | `01`: parameter value 1 33 | 34 | `360300D601` = set DHW setpoint to 47°C
35 | `36`: packet type 0x36
36 | `0300`: parameter number 03
37 | `D601`: parameter value 01D6 HEX = 470 DEC 38 | 39 | `360800F6FF` = set LWT setpoint deviation to -1°C
40 | `36`: packet type 0x36
41 | `0800`: parameter number 03
42 | `F6FF`: parameter value FFF6 HEX = -10 DEC 43 | 44 | The P1P2 bus is much slower than UDP or Serial, therefore incoming commands are temporarily stored in a queue (circular buffer). 45 | 46 | ## Daikin Altherma Hybrid and Daikin Altherma LT protocol data format 47 | 48 | Daikin P1P2 protocol payload data format for Daikin Altherma Hybrid (perhaps all EHYHB(H/X) models) and Daikin Altherma LT (perhaps all EHV(H/X) models). Big thanks go to Arnold Niessen for his development of the P1P2 adapter and the P1P2Serial library. This document is based on reverse engineering and assumptions, so there may be mistakes and misunderstandings. 49 | 50 | This document describes payload data of few **selected writeable packet types** (packet types 0x35, 0x36 and 0x3A) exchanged between the **main controller (requests)** and the **external controller (responses)**. 51 | 52 | Payload of these packets has specific structure, it contains pairs of **parameter number** (2 bytes) + **parameter value** (1 or 2 bytes). Parameter numbers and values use little endian bytes order. Since the payload length is fixed, each data packet can hold only a limited number of these number-value pairs. Empty space in the payload is filled with 0xFF. 53 | 54 | ### Data types 55 | 56 | The following data types were observed in the parameter values. Little endian bytes ordering is used in multi-byte data types: 57 | 58 | | Data type | Definition (read) | Definition (write) | 59 | | ----------- | ------------------------------------- | ------------------------------------- | 60 | | u8 | unsigned 8-bit integer 0 .. 255 | | 61 | | s16 | signed 16-bit integer -32768..32767 | | 62 | | u8div2min16 | unsigned 8-bit integer 0 .. 255, divide by 2 and deduct 16 | multiply by 2 and add 32 | 63 | 64 | 65 | Explanation of **s16** format: a temperature of 21.5°C is represented by the value of 215 in little endian format (0xD700). A temperature of -1°C is represented by the value of -10 in little endian format (0xF6FF). 66 | 67 | Observations show that a few hundred parameters can be exchanged via packet types 0x3X, but only some of them are writeable. The following tables summarize all known writeable parameters for Daikin Altherma LT and Altherma Hybrid: 68 | 69 | ### Packet type 0x35 70 | 71 | | Parameter number | Description | Applies for | Data type | Byte: description | 72 | | ---------------- | ------------------------ | ----------- | --------- | ------------------------------ | 73 | | 03 | Quiet mode | | u8 | 0x00: off
0x01: on | 74 | | 2F | Climate control | LWT mode | u8 | 0x00: off
0x01: on | 75 | | 31 | Climate control | RT mode | u8 | 0x00: off
0x01: on | 76 | | 3A* | Heating/cooling | | u8 | 0x01: heating
0x02: cooling | 77 | | 36 | Defrost request | | u8 | 0x00: off
0x01: on (request) | 78 | | 40 | DHW control | | u8 | 0x00: off
0x01: on | 79 | | 48 | DHW boost | | u8 | 0x00: off
0x01: on | 80 | | 56 | Weather dependent / Fixed mode | LWT mode | u8 | 0x00: fixed
0x01: weather dep.
0x02: fixed+scheduled
0x03: weather dep.+scheduled | 81 | 82 | *not working correctly, use packet type 0x3A, parameter 4E instead 83 | 84 | ### Packet type 0x36 85 | 86 | All temperature values in this table are in 0.1 °C resolution. 87 | 88 | | Parameter number | Description | Applies for | Data type | Byte: description | 89 | | ---------------- | ---------------------------------------- | ----------- | --------- | ----------------- | 90 | | 00 | Room heating setpoint | RT mode | s16 | | 91 | | 01 | Room cooling setpoint | RT mode | s16 | | 92 | | 03 | DHW setpoint | | s16 | | 93 | | 06 | LWT heating setpoint (main zone) | LWT - Fixed mode | s16 | | 94 | | 07 | LWT cooling setpoint (main zone) | LWT - Fixed mode | s16 | | 95 | | 08 | LWT heating deviation (main zone) | LWT - WD mode | s16 | | 96 | | 09 | LWT cooling deviation (main zone) | LWT - WD mode | s16 | | 97 | | 0B | LWT heating setpoint (additional zone) | LWT - Fixed mode | s16 | | 98 | | 0C | LWT cooling setpoint (additional zone) | LWT - Fixed mode | s16 | | 99 | | 0D | LWT heating deviation (additional zone) | LWT - WD mode | s16 | | 100 | | 0E | LWT cooling deviation (additional zone) | LWT - WD mode | s16 | | 101 | 102 | 103 | 104 | ### Packet type 0x3A 105 | 106 | | Parameter number | Description | Data type | Byte: description | 107 | | ---------------- | ---------------------- | --------- | ------------------------------------------------------------ | 108 | | 00 | 12h/24h time format | u8 | 0x00: 12h time format
0x01: 24h time format | 109 | | 31 | Enable holiday ?? | u8 | ?? | 110 | | 39 | Preset LWT deviation heating comfort | u8div2min16 | | 111 | | 3A | Preset LWT deviation heating eco | u8div2min16 | | 112 | | 3B | Decimal delimiter | u8 | 0x00: dot
0x01: comma | 113 | | 3D | Flow units | u8 | 0x00: l/min
0x01: GPM | 114 | | 3F | Temperature units | u8 | 0x00: °F
0x01: °C | 115 | | 40 | Energy units | u8 | 0x00: kWh
0x01: MBtu | 116 | | 45 | Preset room cooling comfort | u8div2min16 | | 117 | | 46 | Preset room cooling eco | u8div2min16 | | 118 | | 47 | Preset room heating comfort | u8div2min16 | | 119 | | 48 | Preset room heating eco | u8div2min16 | | 120 | | 49 | Preset mode | u8 | 0x00: schedule
0x01: eco
0x02: comfort | 121 | | 4B | Daylight saving time | u8 | 0x00: manual
0x01: auto | 122 | | 4C | Quiet mode | u8 | 0x00: auto
0x01: always off
0x02: on | 123 | | 4D | Quiet mode level | u8 | 0x00: level 1
0x01: level 2
0x02: level 3 (most silent) | 124 | | 4E | Operation mode | u8 | 0x00: heating
0x01: cooling
0x02: auto | 125 | | 5B | Holiday | u8 | 0x00: off
0x01: on | 126 | | 5E | Heating schedule | u8 | 0x00: Predefined 1
0x01: Predefined 2
0x02: Predefined 3
0x03: User defined 1
0x04: User defined 2
0x05: User defined 3
0x06: No schedule
| 127 | | 5F | Cooling schedule | u8 | 0x00: Predefined 1
0x01: Predefined 2
0x02: Predefined 3
0x03: User defined 1
0x04: No schedule
| 128 | | 64 | DHW schedule | u8 | 0x00: Predefined 1
0x01: Predefined 2
0x02: Predefined 3
0x03: User defined 1
0x04: No schedule | 129 | 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Arduino Altherma UDP Controller 2 | 3 | * [What is it good for?](#what-is-it-good-for) 4 | * [Technical specifications](#technical-specifications) 5 | * [Hardware](#hardware) 6 | * [Firmware](#firmware) 7 | * [Settings](#settings) 8 | - [System Info](#system-info) 9 | - [P1P2 Status](#p1p2-status) 10 | - [IP Settings](#ip-settings) 11 | - [TCP/UDP Settings](#tcpudp-settings) 12 | - [P1P2 Settings](#p1p2-settings) 13 | - [Packet Filter](#packet-filter) 14 | - [Tools](#tools) 15 | * [Integration](#integration) 16 | - [Loxone](#loxone) 17 | - [Other systems](#other-systems) 18 | * [Limitations and known issues](#limitations-and-known-issues) 19 | * [Comparison with other solutions](#comparison-with-other-solutions) 20 | * [Version history](#version-history) 21 | 22 | # What is it good for? 23 | 24 | Allows you to connect your Daikin Altherma heat pump (P1/P2 bus) to a home automation system (such as Loxone). This controller reads data from the P1/P2 bus on your Daikin Altherma and forwards them via ethernet UDP. You can also control your Daikin Altherma by sending commands via ethernet UDP. 25 | 26 | The controller has a built-in web interface. You can use this web interface to configure the controller itself, monitor the status of the controller's connection to the P1/P2 bus and check error counters. All settings and P1/P2 statistics are stored in EEPROM. This program implements P1P2Serial library (https://github.com/Arnold-n/P1P2Serial). 27 | 28 | # Technical specifications 29 | 30 | * this controller is compatible with Daikin Altherma heat pumps (E-series), including: 31 | - Altherma Hybrid 32 | - Altherma 33 | - Altherma 3 34 | * connects to Daikin Altherma via P1/P2 interface 35 | * connects to home automation system via local ethernet 36 | * communication protocol: 37 | - UDP (raw HEX data) 38 | * diagnostics via built-in web interface: 39 | - send P1/P2 command directly via web interface 40 | - P1/P2 statistics, counters for packets read from and written to P1/P2, counters for errors (counters are saved to EEPROM every 6 hours) 41 | - rollover of counters is synchronized 42 | - content of the P1P2 Status page is updated in the background (fetch API), javascript alert is shown if connection is lost 43 | * user settings: 44 | - can be changed via web interface (see screenshots below), all web UI inputs have proper validation 45 | - stored in Arduino EEPROM 46 | - retained during firmware upgrade (only in case of major version change, Arduino loads factory defaults) 47 | - factory defaults for user settings can be changed in advanced_settings.h 48 | * advanced settings: 49 | - can be changed in sketch before compilation (advanced_settings.h) 50 | - stored in flash memory 51 | 52 | # Hardware 53 | Get the hardware and connect together: 54 | 55 | * **Arduino Uno or Mega** (and possibly other boards with ATmega chips).
On Mega you have to configure Serial in advanced settings in the sketch. 56 | * **Ethernet shield with WIZnet chip (W5100, W5200 or W5500)**.
The ubiquitous W5100 shield for Uno/Mega is sufficient. If available, I recommend W5500 Ethernet Shield. You can also use combo board MCU + ethernet (such as ATmega328 + W5500 board from Keyestudio).
ATTENTION: Ethernet shields with ENC28J60 chip will not work !!! 57 | * **Custom P1P2 Uno adapter**.
You can [solder your own adapter](https://github.com/Arnold-n/P1P2Serial/tree/main/circuits#p1p2-adapter-as-arduino-uno-hat) or buy one from Arnold-n (his e-mail address can be found on line 3 of his library [P1P2Serial.cpp](https://github.com/Arnold-n/P1P2Serial/blob/main/P1P2Serial.cpp)). 58 | 59 | Here is my HW setup (cheap Arduino Uno clone + W5500 Ethernet shield from Keyestudio + custom P1P2 Uno adapter): 60 | 61 | HW 62 | 63 | # Firmware 64 | 65 | You can either: 66 | - **Download and flash my pre-compiled firmware** from "Releases". 67 | - **Compile your own firmware**. Download this repository (all *.ino files) and open arduino-modbus-rtu-tcp-gateway.ino in Arduino IDE. If you want, you can check advanced_settings.h for advanced settings (can only be changed in the sketch) and for default factory settings (can be later changed via web interface). Download all required libraries, compile and upload your program to Arduino. The program uses the following external libraries (all are available in Arduino IDE's "library manager"): 68 | - CircularBuffer (https://github.com/rlogiacco/CircularBuffer) 69 | - StreamLib (https://github.com/jandrassy/StreamLib) 70 | 71 | Connect your Arduino to ethernet and use your web browser to access the web interface on default IP: http://192.168.1.254 72 | Enjoy :-) 73 | 74 | # Settings 75 | 76 | This controller has a built-in webserver that allows you to configure the controller itself, check basic system info of the controller and the status of its connection to the P1/P2 bus. 77 | 78 | - settings marked \* are only available if ENABLE_DHCP is defined in advanced_settings.h 79 | - settings marked \*\* are only available if ENABLE_EXTENDED_WEBUI is defined in advanced_settings.h 80 | 81 | ## System Info 82 | 83 | daikin1 84 | 85 | **EEPROM Health**. Keeps track of EEPROM write cycles (this counter is persistent, never cleared during factory resets). Replace your Arduino once you reach 100 000 write cycles (with 6 hours EEPROM_INTERVAL you have more than 50 years lifespan). 86 | 87 | **Ethernet Chip**. Wiznet chip on the ethernet shield. 88 | 89 | **MAC Address**.\*\* First 3 bytes are fixed 90:A2:DA, remaining 3 bytes are random. You can also set manual MAC in IP Settings. 90 | 91 | ## P1P2 Status 92 | 93 | daikin2 94 | 95 | **Daikin Indoor Unit**. Shows the name of your Altherma indoor unit. 96 | 97 | **Daikin Outdoor Unit**.\*\* Shows the name of your Altherma outdoor unit. 98 | 99 | **Date**. Displays internal date and time of the heat pump. 100 | 101 | **This Controller**. Shows the status of this Altherma UDP Controller. These messages can show up: 102 | * **No connection to the P1P2 bus**. No data packets were read from the P1/P2 bus. Check your connection to the P1/P2 bus. 103 | * **Connected (read only)**. The controller is connected in read only mode. It does not write anything to the P1/P2 bus but it still passively monitors the P1/P2 bus and sends (most) data from the heat pump via UDP messages. If your **Enable Write to P1P2** setting is set to *Manually*, you can manually enable write mode (if your pump supports additional device on the P1/P2 bus). Read only mode has several limitations: 104 | - does not show Altherma model in **Daikin Unit** 105 | - can not periodically request, read (and send via UDP) Altherma counters 106 | - can not control Altherma by sending **Write Command** through the web interface 107 | - can not control Altherma by sending commands via UDP 108 | * **Connected (address 0xF..)**. This Arduino device is connected to the P1/P2 bus for both reading and writing (sending commands). The controller can write to the P1/P2 bus only after it has been allocated an address by the heat pump (Arduino will accept any address in the 0xF0 ~ 0xFF range). If your **Enable Write to P1P2** setting is set to *Manually*, you can manually disable write mode (release the address) and downgrade the connection to read only. 109 | 110 | **Other Controllers**. Shows all other external controllers connected to the P1/P2 bus (incl. their addresses) and provides info whether additional controller is supported by your heat pump. These messages can show up: 111 | * **Another device is connected (address 0xF..)**. Another device is connected to the P1/P2 bus using address 0xF.. This "another device" can be second Arduino device, commercial controller by Daikin or by third party (Daikin LAN adapter, Daikin Madoka, DCOM LT/MB, Zennio KLIC-DA KNX, Coolmaster, etc.). 112 | * **Additional device can be connected (address 0xF..)**. Additional device can be connected to the P1/P2 bus. How many devices can be connected (ie. how many addresses are available for external devices) depends on the model of the heat pump. For example, Altherma LT supports only 1 device (address 0xF0), Altherma 3 support up to 3 devices (addresses 0xF0, 0xF1 and 0xFF). 113 | * **Additional device not supported by the pump**. All available addresses have been allocated to external controllers. The heat pump (the main Daikin controller) does not support additional device on the P1/P2 bus. 114 | 115 | **Write Command**. You can send a P1/P2 write command directly from web interface, for testing or reverse-engineering P1/P2 write commands. For the list of commands identified in the P1/P2 protocol (through reverse engineering) see [Payload-data-write.md](Payload-data-write.md). The format of the write command send via web interface is identical to the command sent via UDP: 116 | * **Packet Type**. The first byte is the packet type. Only supported packet types are listed in the drop-down menu. 117 | * **Param**. Parameter number, two bytes **in little endian format**! For example, parameter number 03 is inserted as `03` `00`. 118 | * **Value**. Parameter value, the number of bytes differs for various packet types. See PACKET_PARAM_VAL_SIZE in advanced settings for the correct number of bytes. Value is also **in little endian format**! 119 | 120 | **Daikin EEPROM Writes**. Every time you send **Write Command** through the web interface or a command via UDP, settings of the main Daikin controller (= controller on your heat pump) change and new values are written to its internal EEPROM. **Your main Daikin controller's EEPROM has a limited number of writes, so keep an eye on this counter in order to prevent EEPROM wear! It is adviced to do max 7000 writes per year (19 writes/day on average)**. 121 | * **Stats since ...**. Date and time since when **Daikin EEPROM Writes** are recorded. If you significantly change the date on the heat pump, reset the stats (so that **Average per Day** is calculated properly). 122 | * **Commands Sent**. Total number of writes made by this Arduino controller since the date and time recorded in **Stats since ...**. Click **Reset** to reset this stat. 123 | * **Dropped**. Daily EEPROM Write Quota (configured in **P1P2 Settings**) was reached. The command (received via UDP or from the web interface) was dropped. Click **Clear Quota** to reset this stat. 124 | * **Invalid**. Command received via UDP or from the web interface was invalid, it was dropped. Possible reasons: 125 | - Packet type (first byte) is not supported (PACKET_PARAM_VAL_SIZE in advanced settings is set to zero). 126 | - Incorrect packet length. Command should have 1 byte for type, 2 bytes for parameter number and the correct numer of bytes for the parameter value (see PACKET_PARAM_VAL_SIZE in advanced settings). 127 | - Internal queue (circular buffer) for commands is full. 128 | * **Daily Average**. Daily average EEPROM writes, should be below 19. Calculated from internal date of the heat pump, so if you change the date in heat pump settings, it is recommended to reset the Daikin EEPROM Writes counter. 129 | * **Yesterday**. Number of writes made yesterday, updated at midnight. Should not significantly exceed average writes per day. 130 | * **Today**. Number of writes made today out of daily **EEPROM Write Quota**. If you reach the quota and you still need to send a P1/P2 write command, you can **Clear Quota** 131 | 132 | **P1P2 Packets**.\*\* Counters for packets read from the P1/P2 bus or written to the P1/P2 bus. If any of the counters rolls over the unsigned long maximum (4,294,967,295), all counters will reset to 0. 133 | * **Read OK**. Number of packets read from the P1/P2 bus, without errors. Not all of them are sent via UDP (see the **Packet Filter** settings). Packets are read from the P1/P2 bus (and sent via UDP) even if the controller is not connected to the P1/P2 bus. 134 | * **Read Error**. Error while attempting to read packet from the P1/P2 bus. Possible reasons: 135 | - Packet received is longer than the read buffer. 136 | - Parity error detected. 137 | - Buffer overrun detected (overrun is after, not before, the read byte). 138 | - CRC error detected in readpacket. 139 | * **Write OK**. Number of packets written to the P1/P2 bus. Writing to the P1/P2 bus is only possible after the controller receives an address from the pump (from the main Daikin controller). Several types of packets are be written to the P1/P2 bus: 140 | - Regular responses to heat pump's polling of external controllers (keep alive). 141 | - Requests for the counters packets. If the **Enable Write to P1P2** setting is set to *Automatically*, requests for counter packets are sent even if the controller failed to receive an address (is in read only mode). 142 | - Write commands received via web interface or UDP. These packets are written to the P1/P2 bus and written in Daikin EEPROM. 143 | * **Write Error**. Error while attempting to write packet to the P1/P2 bus. Possible reasons: 144 | - Start bit error during write. 145 | - Data read-back error, most probably caused by bus collision. 146 | - High bit half read-back error, most probably caused by bus collision. 147 | 148 | 149 | **UDP Messages**.\*\* 150 | * **Sent to UDP**. Counts packets (messages) read from the P1/P2 bus and sent via UDP. Not all packets read from the P1/P2 bus are sent via UDP (see the **Packet Filter** settings). 151 | * **Received from UDP**. Counts all messages received via UDP from a valid remote IP. 152 | 153 | ## IP Settings 154 | 155 | daikin3 156 | 157 | **MAC Address**. Change MAC address. **Randomize** button will generate new random MAC (first 3 bytes fixed 90:A2:DA, last 3 bytes will be random). 158 | 159 | **Auto IP**.\* Once enabled, Arduino will receive IP, gateway, subnet and DNS from the DHCP server. 160 | 161 | **Static IP**. Set new static IP address. Automatically redirect the web interface to the new IP. 162 | 163 | **Submask**. 164 | 165 | **Gateway**. 166 | 167 | **DNS**.\* 168 | 169 | ## TCP/UDP Settings 170 | 171 | daikin4 172 | 173 | **Remote IP**. IP address of your home automation system that listens for UDP messages and sends UDP commands. 174 | 175 | **Send and Receive UDP**. 176 | * **Only to/from Remote IP**. Only accept UDP messages from the **Remote IP**, send UDP messages directly (unicast) to the **Remote IP**. 177 | * **To/From Any IP (Broadcast)**. Accept UDP messages from any IP, send UDP messages as UDP broadcast. UDP broadcast is faster than UDP unicast. **Remote IP** setting has no effect. 178 | 179 | **UDP Port**. Local UDP port and remote UDP port. 180 | 181 | **WebUI Port**. Change web UI port, automatically redirects the web interface to the new Web UI port. 182 | 183 | ## P1P2 Settings 184 | 185 | daikin5 186 | 187 | **Enable Write to P1P2**. 188 | * **Manually** (default). User can manually enable / disable writing to the P1/P2 bus (see the **P1P2 Status** page). After reboot or if connection is interrupted (see **Connection Timeout**), connection is downgraded to read only. 189 | * **Automatically**. The controller tries to (re)enable writing to the P1/P2 bus after (re)start, or if connection is interrupted. Even if the controller fails to receive an address from the heat pump, it will write counter requests to the P1/P2 bus (address is not needed for sending counter requests). Use *Automatically* at your own risk and only after you have successfuly enabled write manually! **Check the P1P2 Status page for P1/P2 errors and monitor Daikin EEPROM Writes in order to minimize Daikin controller EEPROM wear!** 190 | 191 | **Connection Timeout**. Timeout for reading data packets and for enabling writing to the P1/P2 bus. 192 | * If no data packet is received after timeout, **No connection to the P1P2 bus** message is displayed in the **P1P2 Status** page. 193 | * In **Manual Connect** mode, user initiated attempt to enable writing to the P1/P2 bus (to the main Daikin controller) fails if the controller does not receive an address within the **Connection Timeout**. 194 | * During operation, connection can be downgraded to read only if the controller loses its address for a period longer than the **Connection Timeout** (for example if the address is allocated by the heat pump to another external controller). 195 | 196 | **Daikin EEPROM Write Quota**. Daily quota for writes to the EEPROM of the main Daikin controller. Every command sent via web interface (**Write Command** on **P1P2 Status** page) or via UDP = write cycle to the Daikin EEPROM. If the daily quota is reached, new commands are dropped. The quota resets at midnight or manually on the **P1P2 Status** page. 197 | 198 | **Target Temperature Hysteresis**. Hysteresis for writing target temperature or target setpoint commands (packet type 0x36) in °C. The purpose is to minimize Daikin controller EEPROM wear. Applies for write commands received via UDP: 199 | - Deviation_LWT_Zone_Add 200 | - Deviation_LWT_Zone_Main 201 | - Target_Setpoint_LWT_Zone_Add 202 | - Target_Setpoint_LWT_Zone_Main 203 | - Target_Temperature_DHW 204 | - Target_Temperature_Room 205 | 206 | ## Packet Filter 207 | 208 | daikin6 209 | 210 | The **Packet Filter** page lists all packet types observed on the P1/P2 bus. Some of them are exchanged between the heat pump and the main Daikin controller, others are exchanged between our controller (or other external controllers) and the main Daikin controller. If you do not see any packet types, wait few seconds. If a new packet type is detected, it will be automatically added to the list. **Packet types enabled on this page are forwarded via UDP**. By default, only Counter Packet (0xB8) and Data Packets (usually 0x10 - 0x16) are sent via UDP in order to reduce the UDP traffic. Enable additional packet types if you want to test or reverse-engineer the P1/P2 protocol. 211 | 212 | **Send All Packet Types**. All packets read from the P1/P2 bus are sent via UDP (including packet types that were not yet observed). There is a lot of communication going on on the P1/P2 bus, so use with caution! 213 | 214 | **Counters Packet**. Counter packet is periodically requested by the controller (only works if the controller is connected to the P1/P2 bus). Set the period for the counter packet requests. 215 | 216 | **Data Packets**. 217 | * **Always Send (~770ms cycle)**. Data packets are always sent via UDP, whenever they are read from the P1/P2 bus. Data packets are regularly exchanged between the heat pump and the main Daikin controller every 770ms. 218 | * **If Payload Changed or When Counters Requested**. The controller stores data packet payloads in its RAM. Data packets are sent via UDP: 219 | - if their payload changed 220 | - or when the counters packet is requested (see the counters packet request period) 221 | * **Only If Payload Changed**. Data packets are sent via UDP only if their payload changed. 222 | 223 | ## Tools 224 | daikin7 225 | 226 | **Load Default Settings**. Loads default settings (see DEFAULT_CONFIG in advanced settings). MAC address is retained. 227 | 228 | **Reboot**. 229 | 230 | # Integration 231 | 232 | This controller is mainly intended for the integration with Loxone home automation system. 233 | 234 | ## Loxone 235 | https://user-images.githubusercontent.com/6001151/232344216-52fd6a9e-4cc3-4d51-8f66-87266d960757.mp4 236 | ### 1. Wiring 237 | 238 | It is advised to connect/disconnect devices to the P1/P2 bus only if the power of all connected devices is switched off. 239 | 240 | ### 2. Controller Settings 241 | 242 | The controller can passively read (monitor) most data from the P1/P2 bus while being in a **Disconnected** state. If you also want to write to the bus and control your heat pump, you can (at your own risk!) connect the controller to the P1/P2 bus (to the main Daikin controller). Connect the controller manually, if no errors show up on the P1P2 Status page, enable **Auto Connect**. 243 | 244 | Optionally, set the **Remote IP** (= Loxone Miniserver IP) and enable **Only to/from Remote IP**. See [Remote IP Settings on the W5500 Chip](#remote-ip-settings-on-the-w5500-chip). 245 | 246 | ### 3. Virtual UDP Input 247 | 248 | Download the Loxone template **[VIU_Daikin Altherma.xml](https://github.com/budulinek/arduino-altherma-controller/blob/main/VIU_Daikin%20Altherma.xml)**. Open Loxone Config and in the periphery tree mark `Virtual Inputs`, then go to UDP Device Templates > Import Template ... in the menu bar: 249 | 250 | 251 | 252 | Find the template you have downloaded and import the template. You should see Daikin Altherma with a number of `Virtual UDP Input Commands`. Change the Sender IP address (= IP of your Arduino controller) or UDP port if needed. 253 | 254 | 255 | 256 | Just drag and drop individual inputs into your Loxone plan and you are ready to go. There are only two challenges: 257 | * LWT deviations `Deviation_LWT__` are split into two inputs carrying positive values `_Pos` and negative values `_Neg`. 258 | * There are compound inputs containing multiple digital inputs in one byte. Names of these compound inputs end with `_B`. You need to connect these compound inputs to a `Binary Decoder` block and decode individual digital inputs according to the provided hint. Here is an example of `Valves_B` compound input: 259 | 260 | 261 | 262 | ### 4. Virtual Output 263 | 264 | Virtual Output will only work if the Arduino controller is fully connected to the P1/P2 bus (can write to the bus). Moreover, writeable command are device-specific. The Virtual Output Commands provided in the template may (with no guarantee) work only with **Daikin Altherma LT (EHVH(H/X/Z))** heat pumps. **Use at your own risk!!!** 265 | 266 | Download the Loxone template **[VO_Daikin Altherma LT.xml](https://github.com/budulinek/arduino-altherma-controller/blob/main/VO_Daikin%20Altherma%20LT.xml)**. Open Loxone Config and in the periphery tree mark `Virtual Outputs`, then go to Device Templates > Import Template ... in the menu bar. Find the template you have downloaded and import the template. You should see Daikin Altherma LT with a number of `Virtual Output Commands`. Change the IP address or UDP port if needed: 267 | 268 | 269 | 270 | There are 3 types of outputs available: 271 | 272 | * Analog outputs for target temperatures, setpoints etc. As expected, these outputs accept temperature in °C (float). 273 | * Digital outputs have `_OnOff` in their name. As expected: 0 = Off, 1 = On. 274 | * Discreet analog outputs for various operational modes. These outputs accept discreet integer values - see the provided hints. Here is an example of Heating/Cooling operational mode: 275 | 276 | 277 | If you want to control your heat pump through Loxone App (just like I did in the short video above) or Loxone Web Interface, use the `EIB push-button` block. It has the same functionality as a normal push-button but in addition has the State (S) input that can forward the status of a heat pump settings without triggering an action on the output. As a result, you can have a smooth two-directional communication between the main Daikin controller and the Loxone App. Here you have a solution for the DHW On/Off push-button and the DHW Boost push button. Please note that the DHW_OnOff_B input is a compound input that needs to be decoded with `Binary Decoder`: 278 | 279 | ### 5. Digital Outputs (Relays) 280 | 281 | By default, the main Daikin controller (the user interface mounted at the indoor unit) acts as a thermostat. It has a temperature sensor and can control the heat pump by comparing the actual room temperature with target room temperature. This is not very practical solution if the main Daikin controller is located at the unit that sits somewhere in a separate boiler room. 282 | 283 | Fortunately, 1) Daikin Altherma allows you to connect an external thermostat and 2) Loxone Miniserver can act as an external thermostat. 284 | 285 | **Wiring**. External thermostat is an external 230V relay operated by a temperature sensor (+ some scheduling logic). It controls the heat pump by opening and closing a **230V circuit!** **Danger of death!** Always unplug your heat pump from the mains before connecting wires and consult your heat pump's Installation manual in order to locate the correct wiring terminal. **Proceed at your own risk!** If you know what you are doing, connect the correct wiring terminal of your heat pump (on my Altherma I have used the X2M wiring terminal) to Loxone Miniserver relays. You can use separate relays for heating and cooling and separate relays for main LWT zone and additional LWT zone. Therefore, up to 4 external relays can be used depending on the complexity of your heating/cooling system. 286 | 287 | **Heat Pump Configuration**. Enable external thermostat on your heat pump. Consult your installation manual. In my case I have the settings here: `Installer settings > System layout > Standard > [A.2.1.7] Unit control method > Ext RT control`. If you want to use separate relays for heating and cooling, check also `Installer settings > System layout > Options > [A.2.2.4] Contact type main > H/C request`. 288 | 289 | **Loxone Config**. You can now connect your `Digital Outputs` (relays) directly to the `HVAC Controller` block: 290 | 291 | 292 | 293 | It is quite simple. I have a `HVAC Controller` block that serves as a heating and cooling source for 7 `Intelligent Room Controllers` (see the "7 Objects Assigned" note at the bottom of the HVAC block). The HVAC block needs to know the outdoor temperature, so I have connected the `Temperature_Outside_Stabilized` measured by the heat pump. Relay `Ext_Therm_Main_Heating` triggers heating on the main LWT zone (underfloor heating), relay `Ext_Therm_Add_Cooling` triggers cooling on the additional LWT zone (ceiling cooling panels). `Ext_Therm_Main_Cooling` is not connected because I do not use floor for cooling. 294 | 295 | ### 6. Application Recommendations 296 | 297 | **Use relays for heating/cooling requests**. Your Loxone automation system can now control the Altherma heat pump through 2 sets of outputs: Virtual Output Commands (UDP) and Digital Outputs (Relays). Each of them is doing a different thing, the two do not replace but complement each other: 298 | * **Virtual Output Commands** are sent (via UDP) to the Arduino controller and then (via P1/P2 bus) to the main Daikin controller at your indoor unit. These commands allow you to remotely change user settings (Quiet Mode, Schedules), target temperatures (DHW, LWT), turn the DHW on/off, etc. **Whenever you use Virtual Output Commands (UDP), new settings are stored in the main Daikin controller, wearing its EEPROM!** Use these outputs sparingly. For example, the `LWT_Control_OnOff` command completely shuts down heating (though DHW still works). Valves are closed and the heat pump ignores any heating/cooling requests from the internal thermostat (main Daikin controller) or from the external thermostat (Loxone relays). Use this output only if you want to stop the heat pump for longer periods of time (such as holidays). 299 | * **Digital Outputs** allow you to send heating / cooling requests by closing Loxone Miniserver relays. These relays are connected directly to the heat pump's wiring terminals, not to the main Daikin controller. No new data (settings) are stored in the main Daikin controller's EEPROM. You do wear out your Loxone relay a bit, but relays are easier (and cheaper) to replace than the internal EEPROM of your Daikin controller. Use relays for automated heating / cooling requests, they are meant to be used this way. For example (and in contrast to the LWT_Control_OnOff command mentioned above), the `Ext_Therm_Main_Heating` output only sends a request for heating. When the relay is closed (DO = 1), the heat pump knows there is a demand for heating, and starts compressor in order to maintain the leaving water temperature at a certain level. When the relay is open (DO = 0), compressor stops, no heat is produced but the heat pump remains in a stand-by mode, waiting for new heating request. 300 | 301 | **Adjust the heating curve**. Daikin Altherma heat pumps have a built-in heating curve (weather-dependent curve, equithermic curve). Heating curve means that the leaving water temperature (LWT) depends on weather (outdoor temperature). Interestingly, Loxone also has a heating curve, integrated in the `Intelligent Temperature Controller` block. Here are your choices: 302 | 303 | * **Altherma heating curve**. Altherma heating curve (weather-dependent curve) can be enabled/disabled in installer settings. Consult your installation manual, in my case it is here: `Installer settings > Space operation > LWT settings > Main > [A.3.1.1.1] LWT setpoint mode`. By default, heating curve should be enabled (`Weather dep.`). Users can configure parameters of the heating curve (`User settings > Set weather dependent > Main > Set weather-dependent heating`). In this example, Altherma will use outdoor temperature to calculate the LWT, Loxone will only send heating requests through `Ext_Therm_Main_Heating` relay: 304 | 305 | 306 | 307 | * **Loxone heating curve**. Loxone heating curve is more advanced than Altherma heating curve, because Loxone uses more variables to calculate the LWT. Not just the outdoor temperature, but also the target room temperature, the difference between target and actual room temperatures, room heating phase / cooling phase. Moreover, Loxone can also adjust the target room temp (thus the requested LWT) based on schedule or presence of people. So, go ahead and completely disable your Altherma's heating curve (set the `LWT setpoint mode` to `Absolute`) and use the Loxone block `Intelligent Temperature Controller` (AQf - Flow Target Temperature) to calculate the LWT. Loxone will send heating requests through `Ext_Therm_Main_Heating` relay and requested leaving water temperature through `Target_Setpoint_LWT_Zone_Main` virtual UDP command. This is the most efficient way of controling your heat pump. The problem is that target LWT values calculated by Loxone change far too often (because outdoor temperature fluctuates throughout the day). Hysteresis setting in Arduino helps you reduce the frequency of LWT values sent to the heat pump, but the number of EEPROM writes will (probably) still exceed the limit (19 writes per day). You can give it a try but I do not recommend this choice. 308 | 309 | 310 | 311 | * **Altherma heating curve + adjustments by Loxone**. The `Intelligent Temperature Controller` block can also output AQi - Flow Temperature Increase/Decrease. This increase/decrease is caused by the room temperature difference (see parameter G - Gain) and the target room temperature increase during heating phase (see parameter I - Target temperature increase). In other words, AQi is Loxone's adjustment to the weather-dependent target LWT. We can sent it to the heat pump through the `Deviation_LWT_Zone_Main` virtual UDP command. This is a compromise solution. We will let Altherma calculate the weather-dependent LWT (from the outdoor temperature) and Loxone will calculate the LWT adjustment (from the difference between target and actual room temperature). Since AQi does not change that often, we will have less EEPROM writes. You can try this choice, but: 312 | - Set the parameter I - Target temperature increase to zero. Daikin user manual explicitly discourages users from increasing/decreasing the desired room temperature to speed up space heating/cooling. Use only the parameter G - Gain. 313 | - Check the number of Daikin EEPROM Writes in the Arduino controller web interface. Make sure that average writes per day (and yesterday writes) remain below the limit (19 writes per day). 314 | 315 | 316 | 317 | ## Other systems 318 | 319 | #### Home Assistant 320 | 321 | Arduino Altherma UDP controller is not suitable for Home Assistant. As far as I know, it is difficult to parse hex UDP messages in HA. Check the table below and use another solution (or Node-RED as an intermediary). 322 | 323 | #### Node-RED 324 | 325 | Import and configure the **[node-red-contrib-buffer-parser](https://flows.nodered.org/node/node-red-contrib-buffer-parser)** package. See [Payload-data-read.md](Payload-data-read.md) how to parse UDP messages (read data from the heat pump). See [Payload-data-write.md](Payload-data-write.md) how to make UDP commands (write to the heat pump). 326 | 327 | You can use Node RED as: 328 | * a rudimentary automation system on its own 329 | * an intermediary between the Arduino controller and other home automation system 330 | * an intermediary between the Arduino controller and a time series database and visualisation tool (InfluxDB + Grafana) 331 | 332 | # Limitations and known issues 333 | 334 | ## Portability 335 | 336 | The code was tested on Arduino Uno, ethernet chips W5100 and W5500. It may work on other platforms, but: 337 | 338 | * The random number generator (for random MAC) is seeded through watch dog timer interrupt - this will work only on Arduino (credits to https://sites.google.com/site/astudyofentropy/project-definition/timer-jitter-entropy-sources/entropy-library/arduino-random-seed) 339 | * The restart function will also work only on Arduino. 340 | 341 | ## Ethernet Power On Reset Issue 342 | 343 | Sometimes the gateway is running fine for days but after power-up or brief loss of power (caused for example by undervoltage), ethernet connection is lost. What is the problem? The W5x00 chip on the Arduino Ethernet Shield is not initialized correctly upon power-up. There is an easy solution to the issue described in a separate [document here](https://github.com/budulinek/arduino-modbus-rtu-tcp-gateway/blob/master/Ethernet_SW_Reset.md). 344 | 345 | ## Remote IP settings on the W5500 Chip 346 | 347 | The Ethernet.setRetransmissionCount() and Ethernet.setRetransmissionTimeout() commands do not work on W5500 chips because of a bug in the Ethernet.h library (see [this issue](https://github.com/arduino-libraries/Ethernet/issues/140)). As a result, Arduino fails to read data from the P1/P2 bus (read errors, CRC errors) if certain conditions are met: 348 | * Ethernet shield with the W5500 chip is used. 349 | * Send and Receive UDP setting is set to **Only to/from Remote IP**. 350 | * Device with the remote IP does not exist on your local LAN. 351 | 352 | In this situation (UDP unicast) the W5500 chip checks whether the remote IP exists via ARP request. While the Ethernet.h waits for the ARP response, new P1/P2 packet arrives and is not properly processed that leads to P1/P2 read errors. The solution is simple. If you have a shield with the W5500 chip, set the Send and Receive UDP setting to **To/From Any IP (Broadcast)**. 353 | 354 | ## Ethernet sockets 355 | 356 | The number of used sockets is determined (by the Ethernet.h library) based on microcontroller RAM. Therefore, even if you use W5500 (with 8 sockets available) on Arduino Nano, only 4 sockets will be used due to limited RAM on Nano. 357 | 358 | ## Memory 359 | 360 | Not everything could fit into the limited flash memory of Arduino Nano / Uno. If you have a microcontroller with more memory (such as Mega), you can enable extra settings in the main sketch by defining ENABLE_DHCP and/or ENABLE_EXTRA_DIAG in advanced settings. 361 | 362 | # Comparison with other solutions 363 | 364 | As of April 2023: 365 | 366 | | **Project** | **[budulinek/
arduino-altherma-controller](https://github.com/budulinek/arduino-altherma-controller)** | **[Arnold-n/
P1P2Serial](https://github.com/Arnold-n/P1P2Serial)** | **[raomin/
ESPAltherma](https://github.com/raomin/ESPAltherma)** | **[tadasdanielius/
daikin_altherma](https://github.com/tadasdanielius/daikin_altherma)** | **[speleolontra/
daikin_residential_altherma](https://github.com/speleolontra/daikin_residential_altherma)** || 367 | |------------------------------------|------------------------------------------------------------------------------------|----------------------------------------------------------|-------------------------------------------------------------------------------|------------------------------------------------------------------------------|------------------------------------------------------------------------------|-------------------------------------------| 368 | | **Hardware** | • Arduino Uno
• Ethernet Shield
• Custom P1P2 Uno adapter | • Custom all-in-one board | • M5StickC (or any ESP32/ESP8266 board)
• external relay (optional) | • Daikin LAN adapter
(BRP069A62/ BRP069A61
with OLD firmware) | • Daikin LAN adapter
(BRP069A62/ BRP069A61
with NEW firmware) | • Daikin WLAN adapter
(BRP069A78) | 369 | | **Programable MCUs** | ATmega328P | ATmega328P + ESP8266 | ESP32/8266 | --- | --- || 370 | | **Connection to Daikin Altherma** | P1/P2 bus | P1/P2 bus | X10A serial port | P1/P2 bus | P1/P2 bus | dedicated slot | 371 | | **Interface** | • Ethernet | • WiFi
• Ethernet (optional) | • WiFi | • Ethernet | • Ethernet | • WiFi | 372 | | **Local LAN or Cloud** | Local | Local | Local | Local | Cloud || 373 | | **Controller configuration** | • web interface | • HA
• console | • sketch | • web interface | • web interface || 374 | | **OTA upgrades** | No | Yes | Yes | Yes | Yes || 375 | | **Read data from Daikin Altherma** | Yes | Yes | Yes | Limited | Limited || 376 | | **Control Daikin Altherma** | Yes | Yes | Limited | Yes | Yes || 377 | | **Communication protocol** | UDP | MQTT | MQTT | Websockets | Websockets || 378 | | **Data format** | HEX | JSON | JSON | JSON | JSON || 379 | | **Integration with** | • Loxone

• other systems (via UDP-HEX) | • Home Assistant

• other systems (via MQTT-JSON) | • Home Assistant

• other systems (via MQTT-JSON) | • Home Assistant | • Home Assistant || 380 | 381 | ## Version history 382 | 383 | For version history see: 384 | 385 | https://github.com/budulinek/arduino-altherma-controller/blob/main/arduino-altherma-controller/arduino-altherma-controller.ino#L3 386 | 387 | -------------------------------------------------------------------------------- /VIU_Daikin Altherma.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 16 | 21 | 22 | 23 | 25 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 59 | 64 | 65 | 66 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /VO_Daikin Altherma LT.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 22 | 25 | 33 | 38 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 54 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 65 | 66 | -------------------------------------------------------------------------------- /arduino-altherma-controller/01-interfaces.ino: -------------------------------------------------------------------------------- 1 | /**************************************************************************/ 2 | /*! 3 | @brief Initiates ethernet interface, if DHCP enabled, gets IP from DHCP, 4 | starts all servers (UDP, web server). 5 | */ 6 | /**************************************************************************/ 7 | void startEthernet() { 8 | #ifdef ETH_RESET_PIN 9 | pinMode(ETH_RESET_PIN, OUTPUT); 10 | digitalWrite(ETH_RESET_PIN, LOW); 11 | delay(25); 12 | digitalWrite(ETH_RESET_PIN, HIGH); 13 | delay(ETH_RESET_DELAY); 14 | #endif 15 | 16 | #ifdef ENABLE_DHCP 17 | dhcpSuccess = false; 18 | if (data.config.enableDhcp) { 19 | dhcpSuccess = Ethernet.begin(data.mac); 20 | } 21 | if (!dhcpSuccess) { 22 | Ethernet.begin(data.mac, data.config.ip, data.config.dns, data.config.gateway, data.config.subnet); 23 | } 24 | #else /* ENABLE_DHCP */ 25 | Ethernet.begin(data.mac, data.config.ip, {}, data.config.gateway, data.config.subnet); // No DNS 26 | #endif /* ENABLE_DHCP */ 27 | 28 | W5100.setRetransmissionTime(TCP_RETRANSMISSION_TIMEOUT); 29 | W5100.setRetransmissionCount(TCP_RETRANSMISSION_COUNT); 30 | webServer = EthernetServer(data.config.webPort); 31 | Udp.begin(data.config.udpPort); 32 | webServer.begin(); 33 | #if MAX_SOCK_NUM > 4 34 | if (W5100.getChip() == 51) maxSockNum = 4; // W5100 chip never supports more than 4 sockets 35 | #endif 36 | } 37 | 38 | /**************************************************************************/ 39 | /*! 40 | @brief Resets Arduino (works only on AVR chips). 41 | */ 42 | /**************************************************************************/ 43 | void (*resetFunc)(void) = 0; //declare reset function at address 0 44 | 45 | /**************************************************************************/ 46 | /*! 47 | @brief Maintains uptime in case of millis() overflow. 48 | */ 49 | /**************************************************************************/ 50 | #ifdef ENABLE_EXTENDED_WEBUI 51 | void maintainUptime() { 52 | uint32_t milliseconds = millis(); 53 | if (last_milliseconds > milliseconds) { 54 | //in case of millis() overflow, store existing passed seconds 55 | remaining_seconds = seconds; 56 | } 57 | //store last millis(), so that we can detect on the next call 58 | //if there is a millis() overflow ( millis() returns 0 ) 59 | last_milliseconds = milliseconds; 60 | //In case of overflow, the "remaining_seconds" variable contains seconds counted before the overflow. 61 | //We add the "remaining_seconds", so that we can continue measuring the time passed from the last boot of the device. 62 | seconds = (milliseconds / 1000) + remaining_seconds; 63 | } 64 | #endif /* ENABLE_EXTENDED_WEBUI */ 65 | 66 | /**************************************************************************/ 67 | /*! 68 | @brief Synchronizes roll-over of data counters to zero. 69 | */ 70 | /**************************************************************************/ 71 | bool rollover() { 72 | const uint32_t ROLLOVER = 0xFFFFFF00; 73 | for (byte i = 0; i < P1P2_LAST; i++) { 74 | if (data.p1p2Cnt[i] > ROLLOVER) { 75 | return true; 76 | } 77 | } 78 | #ifdef ENABLE_EXTENDED_WEBUI 79 | for (byte i = 0; i < UDP_LAST; i++) { 80 | if (data.udpCnt[i] > ROLLOVER) { 81 | return true; 82 | } 83 | } 84 | if (seconds > ROLLOVER) { 85 | return true; 86 | } 87 | #endif /* ENABLE_EXTENDED_WEBUI */ 88 | return false; 89 | } 90 | 91 | /**************************************************************************/ 92 | /*! 93 | @brief Resets P1P2 stats, date and UDP stats. 94 | */ 95 | /**************************************************************************/ 96 | void resetStats() { 97 | memset(data.statsDate, 0, sizeof(data.statsDate)); 98 | memset(data.p1p2Cnt, 0, sizeof(data.p1p2Cnt)); 99 | #ifdef ENABLE_EXTENDED_WEBUI 100 | memset(data.udpCnt, 0, sizeof(data.udpCnt)); 101 | remaining_seconds = -(millis() / 1000); 102 | #endif /* ENABLE_EXTENDED_WEBUI */ 103 | } 104 | 105 | /**************************************************************************/ 106 | /*! 107 | @brief Resets Daikin EEPROM stats. 108 | */ 109 | /**************************************************************************/ 110 | void resetEepromStats() { 111 | memset(&data.eepromDaikin, 0, sizeof(eeprom_t)); 112 | } 113 | 114 | /**************************************************************************/ 115 | /*! 116 | @brief Generate random MAC using pseudo random generator, 117 | bytes 0, 1 and 2 are static (MAC_START), bytes 3, 4 and 5 are generated randomly 118 | */ 119 | /**************************************************************************/ 120 | void generateMac() { 121 | // Marsaglia algorithm from https://github.com/RobTillaart/randomHelpers 122 | seed1 = 36969L * (seed1 & 65535L) + (seed1 >> 16); 123 | seed2 = 18000L * (seed2 & 65535L) + (seed2 >> 16); 124 | uint32_t randomBuffer = (seed1 << 16) + seed2; /* 32-bit random */ 125 | memcpy(data.mac, MAC_START, 3); // set first 3 bytes 126 | for (byte i = 0; i < 3; i++) { 127 | data.mac[i + 3] = randomBuffer & 0xFF; // random last 3 bytes 128 | randomBuffer >>= 8; 129 | } 130 | } 131 | 132 | /**************************************************************************/ 133 | /*! 134 | @brief Write (update) data to Arduino EEPROM. 135 | */ 136 | /**************************************************************************/ 137 | void updateEeprom() { 138 | eepromTimer.sleep(EEPROM_INTERVAL * 60UL * 60UL * 1000UL); // EEPROM_INTERVAL is in hours, sleep is in milliseconds! 139 | data.eepromWrites++; // we assume that at least some bytes are written to EEPROM during EEPROM.update or EEPROM.put 140 | EEPROM.put(DATA_START, data); 141 | } 142 | 143 | 144 | uint32_t lastSocketUse[MAX_SOCK_NUM]; 145 | /**************************************************************************/ 146 | /*! 147 | @brief Closes sockets which are waiting to be closed or which refuse to close, 148 | forwards sockets with data available for further processing by the webserver, 149 | disconnects (closes) sockets which are too old (idle for too long), opens 150 | new sockets if needed (and if available). 151 | From https://github.com/SapientHetero/Ethernet/blob/master/src/socket.cpp 152 | */ 153 | /**************************************************************************/ 154 | void manageSockets() { 155 | uint32_t maxAge = 0; // the 'age' of the socket in a 'disconnectable' state that was last used the longest time ago 156 | byte oldest = MAX_SOCK_NUM; // the socket number of the 'oldest' disconnectable socket 157 | byte webListening = MAX_SOCK_NUM; 158 | byte dataAvailable = MAX_SOCK_NUM; 159 | byte socketsAvailable = 0; 160 | SPI.beginTransaction(SPI_ETHERNET_SETTINGS); // begin SPI transaction 161 | // look at all the hardware sockets, record and take action based on current states 162 | for (byte s = 0; s < maxSockNum; s++) { // for each hardware socket ... 163 | byte status = W5100.readSnSR(s); // get socket status... 164 | uint32_t sockAge = millis() - lastSocketUse[s]; // age of the current socket 165 | switch (status) { 166 | case SnSR::CLOSED: 167 | { 168 | socketsAvailable++; 169 | } 170 | break; 171 | case SnSR::LISTEN: 172 | case SnSR::SYNRECV: 173 | { 174 | lastSocketUse[s] = millis(); 175 | webListening = s; 176 | } 177 | break; 178 | case SnSR::FIN_WAIT: 179 | case SnSR::CLOSING: 180 | case SnSR::TIME_WAIT: 181 | case SnSR::LAST_ACK: 182 | { 183 | socketsAvailable++; // socket will be available soon 184 | if (sockAge > TCP_DISCON_TIMEOUT) { // if it's been more than TCP_CLIENT_DISCON_TIMEOUT since disconnect command was sent... 185 | W5100.execCmdSn(s, Sock_CLOSE); // send CLOSE command... 186 | lastSocketUse[s] = millis(); // and record time at which it was sent so we don't do it repeatedly. 187 | } 188 | } 189 | break; 190 | case SnSR::ESTABLISHED: 191 | case SnSR::CLOSE_WAIT: 192 | { 193 | if (EthernetClient(s).available() > 0) { 194 | dataAvailable = s; 195 | lastSocketUse[s] = millis(); 196 | } else { 197 | // remote host closed connection, our end still open 198 | if (status == SnSR::CLOSE_WAIT) { 199 | socketsAvailable++; // socket will be available soon 200 | W5100.execCmdSn(s, Sock_DISCON); // send DISCON command... 201 | lastSocketUse[s] = millis(); // record time at which it was sent... 202 | // status becomes LAST_ACK for short time 203 | } else if ((W5100.readSnPORT(s) == data.config.webPort && sockAge > WEB_IDLE_TIMEOUT) && sockAge > maxAge) { 204 | oldest = s; // record the socket number... 205 | maxAge = sockAge; // and make its age the new max age. 206 | } 207 | } 208 | } 209 | break; 210 | default: 211 | break; 212 | } 213 | } 214 | 215 | if (dataAvailable != MAX_SOCK_NUM) { 216 | EthernetClient client = EthernetClient(dataAvailable); 217 | recvWeb(client); 218 | } 219 | 220 | if (webListening == MAX_SOCK_NUM) { 221 | webServer.begin(); 222 | } 223 | 224 | // If needed, disconnect socket that's been idle (ESTABLISHED without data recieved) the longest 225 | if (oldest != MAX_SOCK_NUM && socketsAvailable == 0 && webListening == MAX_SOCK_NUM) { 226 | disconSocket(oldest); 227 | } 228 | 229 | SPI.endTransaction(); // Serves to o release the bus for other devices to access it. Since the ethernet chip is the only device 230 | } 231 | 232 | /**************************************************************************/ 233 | /*! 234 | @brief Disconnect or close a socket. 235 | @param s Socket number. 236 | */ 237 | /**************************************************************************/ 238 | void disconSocket(byte s) { 239 | if (W5100.readSnSR(s) == SnSR::ESTABLISHED) { 240 | W5100.execCmdSn(s, Sock_DISCON); // Sock_DISCON does not close LISTEN sockets 241 | lastSocketUse[s] = millis(); // record time at which it was sent... 242 | } else { 243 | W5100.execCmdSn(s, Sock_CLOSE); // send DISCON command... 244 | } 245 | } 246 | 247 | /**************************************************************************/ 248 | /*! 249 | @brief Maintains connection to the P1P2 bus. 250 | */ 251 | /**************************************************************************/ 252 | void manageController() { 253 | uint8_t controllerState = controllerAddr; 254 | // Reset FxRequests periodically 255 | if (p1p2Timer.isOver()) { 256 | memset(FxRequests, 0, sizeof(FxRequests)); 257 | } 258 | // Handle disconnected or connecting states 259 | if (controllerState <= CONNECTING) { 260 | cmdQueue.clear(); 261 | counterRequestTimer.sleep(0); 262 | daikinNameTimer.sleep(0); 263 | if (controllerState == DISCONNECTED || (controllerState == CONNECTING && data.config.controllerMode == CONTROL_AUTO)) { 264 | connectionTimer.sleep(data.config.connectTimeout * 1000UL); 265 | if (data.config.controllerMode == CONTROL_AUTO) { 266 | controllerAddr = CONNECTING; 267 | } 268 | } 269 | } 270 | // Update controller address based on connection timer 271 | if (connectionTimer.isOver()) { 272 | controllerAddr = (data.config.controllerMode == CONTROL_AUTO) ? CONNECTING : DISCONNECTED; 273 | } 274 | // Handle Write mode or Auto mode 275 | if (controllerAddr > CONNECTING || data.config.controllerMode == CONTROL_AUTO) { 276 | // Handle counter requests 277 | if (counterRequestTimer.isOver()) { 278 | counterRequestTimer.sleep(data.config.counterPeriod * 60UL * 1000UL); 279 | cmdQueue.push(2); 280 | cmdQueue.push(PACKET_TYPE_COUNTER); 281 | cmdQueue.push(0); 282 | if (data.config.sendDataPackets == DATA_CHANGE_AND_REQUEST) { 283 | memset(savedPackets, 0xFF, sizeof(savedPackets)); // Reset saved packets 284 | } 285 | } 286 | // Handle Daikin names 287 | if (daikinNameTimer.isOver()) { 288 | daikinNameTimer.sleep(60UL * 1000UL); 289 | if (daikinIndoor[0] == '\0') { 290 | cmdQueue.push(2); 291 | cmdQueue.push(PACKET_TYPE_INDOOR_NAME); 292 | cmdQueue.push(0); 293 | } 294 | #ifdef ENABLE_EXTENDED_WEBUI 295 | if (daikinOutdoor[0] == '\0') { 296 | cmdQueue.push(2); 297 | cmdQueue.push(PACKET_TYPE_OUTDOOR_NAME); 298 | cmdQueue.push(0); 299 | } 300 | #endif 301 | } 302 | } 303 | } 304 | 305 | /**************************************************************************/ 306 | /*! 307 | @brief Seed pseudorandom generator using watch dog timer interrupt (works only on AVR). 308 | See https://sites.google.com/site/astudyofentropy/project-definition/timer-jitter-entropy-sources/entropy-library/arduino-random-seed 309 | */ 310 | /**************************************************************************/ 311 | void CreateTrulyRandomSeed() { 312 | seed1 = 0; 313 | nrot = 32; // Must be at least 4, but more increased the uniformity of the produced seeds entropy. 314 | // The following five lines of code turn on the watch dog timer interrupt to create 315 | // the seed value 316 | cli(); 317 | MCUSR = 0; 318 | _WD_CONTROL_REG |= (1 << _WD_CHANGE_BIT) | (1 << WDE); 319 | _WD_CONTROL_REG = (1 << WDIE); 320 | sei(); 321 | while (nrot > 0) 322 | ; // wait here until seed is created 323 | // The following five lines turn off the watch dog timer interrupt 324 | cli(); 325 | MCUSR = 0; 326 | _WD_CONTROL_REG |= (1 << _WD_CHANGE_BIT) | (0 << WDE); 327 | _WD_CONTROL_REG = (0 << WDIE); 328 | sei(); 329 | } 330 | 331 | ISR(WDT_vect) { 332 | nrot--; 333 | seed1 = seed1 << 8; 334 | seed1 = seed1 ^ TCNT1L; 335 | } 336 | 337 | // Preprocessor code for identifying microcontroller board 338 | #if defined(TEENSYDUINO) 339 | // --------------- Teensy ----------------- 340 | #if defined(__AVR_ATmega32U4__) 341 | #define BOARD F("Teensy 2.0") 342 | #elif defined(__AVR_AT90USB1286__) 343 | #define BOARD F("Teensy++ 2.0") 344 | #elif defined(__MK20DX128__) 345 | #define BOARD F("Teensy 3.0") 346 | #elif defined(__MK20DX256__) 347 | #define BOARD F("Teensy 3.2") // and Teensy 3.1 (obsolete) 348 | #elif defined(__MKL26Z64__) 349 | #define BOARD F("Teensy LC") 350 | #elif defined(__MK64FX512__) 351 | #define BOARD F("Teensy 3.5") 352 | #elif defined(__MK66FX1M0__) 353 | #define BOARD F("Teensy 3.6") 354 | #else 355 | #define BOARD F("Unknown Board") 356 | #endif 357 | #else // --------------- Arduino ------------------ 358 | #if defined(ARDUINO_AVR_ADK) 359 | #define BOARD F("Arduino Mega Adk") 360 | #elif defined(ARDUINO_AVR_BT) // Bluetooth 361 | #define BOARD F("Arduino Bt") 362 | #elif defined(ARDUINO_AVR_DUEMILANOVE) 363 | #define BOARD F("Arduino Duemilanove") 364 | #elif defined(ARDUINO_AVR_ESPLORA) 365 | #define BOARD F("Arduino Esplora") 366 | #elif defined(ARDUINO_AVR_ETHERNET) 367 | #define BOARD F("Arduino Ethernet") 368 | #elif defined(ARDUINO_AVR_FIO) 369 | #define BOARD F("Arduino Fio") 370 | #elif defined(ARDUINO_AVR_GEMMA) 371 | #define BOARD F("Arduino Gemma") 372 | #elif defined(ARDUINO_AVR_LEONARDO) 373 | #define BOARD F("Arduino Leonardo") 374 | #elif defined(ARDUINO_AVR_LILYPAD) 375 | #define BOARD F("Arduino Lilypad") 376 | #elif defined(ARDUINO_AVR_LILYPAD_USB) 377 | #define BOARD F("Arduino Lilypad Usb") 378 | #elif defined(ARDUINO_AVR_MEGA) 379 | #define BOARD F("Arduino Mega") 380 | #elif defined(ARDUINO_AVR_MEGA2560) 381 | #define BOARD F("Arduino Mega 2560") 382 | #elif defined(ARDUINO_AVR_MICRO) 383 | #define BOARD F("Arduino Micro") 384 | #elif defined(ARDUINO_AVR_MINI) 385 | #define BOARD F("Arduino Mini") 386 | #elif defined(ARDUINO_AVR_NANO) 387 | #define BOARD F("Arduino Nano") 388 | #elif defined(ARDUINO_AVR_NG) 389 | #define BOARD F("Arduino NG") 390 | #elif defined(ARDUINO_AVR_PRO) 391 | #define BOARD F("Arduino Pro") 392 | #elif defined(ARDUINO_AVR_ROBOT_CONTROL) 393 | #define BOARD F("Arduino Robot Ctrl") 394 | #elif defined(ARDUINO_AVR_ROBOT_MOTOR) 395 | #define BOARD F("Arduino Robot Motor") 396 | #elif defined(ARDUINO_AVR_UNO) 397 | #define BOARD F("Arduino Uno") 398 | #elif defined(ARDUINO_AVR_YUN) 399 | #define BOARD F("Arduino Yun") 400 | 401 | // These boards must be installed separately: 402 | #elif defined(ARDUINO_SAM_DUE) 403 | #define BOARD F("Arduino Due") 404 | #elif defined(ARDUINO_SAMD_ZERO) 405 | #define BOARD F("Arduino Zero") 406 | #elif defined(ARDUINO_ARC32_TOOLS) 407 | #define BOARD F("Arduino 101") 408 | #else 409 | #define BOARD F("Unknown Board") 410 | #endif 411 | #endif 412 | -------------------------------------------------------------------------------- /arduino-altherma-controller/02-UDP.ino: -------------------------------------------------------------------------------- 1 | byte masks[8] = { 1, 2, 4, 8, 16, 32, 64, 128 }; 2 | 3 | /**************************************************************************/ 4 | /*! 5 | @brief Receives P1P2 command via UDP, calls @ref checkCommand() function. 6 | */ 7 | /**************************************************************************/ 8 | void recvUdp() { 9 | uint16_t udpLen = Udp.parsePacket(); 10 | if (udpLen) { 11 | byte command[1 + 2 + MAX_PARAM_SIZE]; // 1 byte packet type + 2 bytes param number + MAX_PARAM_SIZE bytes param value 12 | if (udpLen > sizeof(command) || (!data.config.udpBroadcast && Udp.remoteIP() != IPAddress(data.config.remoteIp))) { 13 | while (Udp.available()) Udp.read(); 14 | // TODO error: UDP too long or wrong remote IP 15 | return; 16 | } 17 | Udp.read(command, sizeof(command)); 18 | checkCommand(command, byte(udpLen)); 19 | #ifdef ENABLE_EXTENDED_WEBUI 20 | data.udpCnt[UDP_RECEIVED]++; 21 | #endif /* ENABLE_EXTENDED_WEBUI */ 22 | } 23 | } 24 | 25 | /**************************************************************************/ 26 | /*! 27 | @brief Checks P1P2 command, checks availability of queue, stores commands 28 | into queue or records an error. 29 | @param command Command received via UDP or web UI. 30 | @param cmdLen Command length. 31 | */ 32 | /**************************************************************************/ 33 | void checkCommand(byte command[], byte cmdLen) { 34 | // Validate packet type and parameter size 35 | byte packetIndex = command[0] - PACKET_TYPE_CONTROL[FIRST]; 36 | if (command[0] < PACKET_TYPE_CONTROL[FIRST] || command[0] > PACKET_TYPE_CONTROL[LAST] || PACKET_PARAM_VAL_SIZE[packetIndex] == 0 || cmdLen - 3 != PACKET_PARAM_VAL_SIZE[packetIndex]) { 37 | data.eepromDaikin.invalid++; // Write Command Invalid 38 | return; 39 | } 40 | // Check queue availability 41 | if (cmdQueue.available() <= cmdLen) { 42 | data.eepromDaikin.invalid++; // Write Queue Full 43 | return; 44 | } 45 | // Check if parameter has changed 46 | if (!changed36Param(command)) return; 47 | // Push command to queue 48 | cmdQueue.push(cmdLen); // First byte in queue is cmdLen 49 | for (byte i = 0; i < cmdLen; i++) { 50 | cmdQueue.push(command[i]); 51 | } 52 | } 53 | 54 | /**************************************************************************/ 55 | /*! 56 | @brief Deletes command from queue. 57 | */ 58 | /**************************************************************************/ 59 | void deleteCmd() { 60 | byte cmdLen = cmdQueue.first(); 61 | for (byte i = 0; i <= cmdLen; i++) { 62 | cmdQueue.shift(); 63 | } 64 | } 65 | 66 | /**************************************************************************/ 67 | /*! 68 | @brief Checks whether the packet type has specific status. 69 | @param packetType Packet type. 70 | @param status Status we are inquiring 71 | - @c PACKET_SEEN Packets of this type have already been detected on the bus 72 | - @c PACKET_SENT Packets of this type will be sent to UDP 73 | @return True if the packet type has specific status 74 | */ 75 | /**************************************************************************/ 76 | bool getPacketStatus(const byte packetType, const byte status) { 77 | return (data.config.packetStatus[status][packetType / 8] & masks[packetType & 7]) > 0; 78 | } 79 | 80 | /**************************************************************************/ 81 | /*! 82 | @brief Sets status for the packet type. 83 | @param packetType Packet type. 84 | @param status Status we are setting 85 | - @c PACKET_SEEN Packets of this type have already been detected on the bus 86 | - @c PACKET_SENT Packets of this type will be sent to UDP 87 | @param value True or false 88 | @return True if value changed 89 | */ 90 | /**************************************************************************/ 91 | bool setPacketStatus(const byte packetType, byte status, const bool value) { 92 | if (getPacketStatus(packetType, status) == value) return false; 93 | if (value == 0) { 94 | data.config.packetStatus[status][packetType / 8] &= ~masks[packetType & 7]; 95 | } else { 96 | data.config.packetStatus[status][packetType / 8] |= masks[packetType & 7]; 97 | } 98 | return true; 99 | } 100 | -------------------------------------------------------------------------------- /arduino-altherma-controller/03-P1P2.ino: -------------------------------------------------------------------------------- 1 | static byte WB[WB_SIZE]; 2 | static byte RB[RB_SIZE]; 3 | static errorbuf_t EB[RB_SIZE]; 4 | 5 | /**************************************************************************/ 6 | /*! 7 | @brief Receives data from the P1P2 bus, calls other functions to parse data, 8 | writes to the P1P2 bus and processes errors. 9 | */ 10 | /**************************************************************************/ 11 | void recvBus() { 12 | while (P1P2Serial.packetavailable()) { 13 | uint16_t delta = 0; 14 | errorbuf_t readError = 0; 15 | uint16_t nread = P1P2Serial.readpacket(RB, delta, EB, RB_SIZE, CRC_GEN, CRC_FEED); 16 | if (nread > RB_SIZE) { 17 | // Received packet longer than RB_SIZE 18 | #ifdef ENABLE_EXTENDED_WEBUI 19 | data.p1p2Cnt[P1P2_READ_ERROR]++; 20 | #endif /* ENABLE_EXTENDED_WEBUI */ 21 | nread = RB_SIZE; 22 | readError = 0xFF; 23 | } 24 | for (uint16_t i = 0; i < nread; i++) readError |= EB[i]; 25 | 26 | if (!readError) { 27 | // message received, no error detected, forward to UDP and parse some info about the heat pump (name, date etc.) 28 | processParseRead(nread, delta); 29 | 30 | // timer to monitor P1P2 messages (reading from bus) 31 | p1p2Timer.sleep(data.config.connectTimeout * 1000UL); 32 | 33 | // act as auxiliary controller: 34 | if (P1P2Serial.writeready() && (controllerAddr > CONNECTING) && (RB[0] == 0x00) && (RB[1] == controllerAddr)) { 35 | 36 | connectionTimer.sleep(data.config.connectTimeout * 1000UL); 37 | //if 1) the main controller sends request to our auxiliary controller 2) we are write ready => always respond 38 | processWrite(nread); 39 | } 40 | } else { 41 | #ifdef ENABLE_EXTENDED_WEBUI 42 | processErrors(nread); 43 | #endif /* ENABLE_EXTENDED_WEBUI */ 44 | } 45 | } 46 | } 47 | 48 | /**************************************************************************/ 49 | /*! 50 | @brief Forwards packets read from the P1P2 bus to UDP, reads some important 51 | variables (date, unit name, etc.), checks for other auxiliary controllers 52 | and gets controller address. 53 | */ 54 | /**************************************************************************/ 55 | void processParseRead(uint16_t n, uint16_t delta) { 56 | if (CRC_GEN) n--; // omit CRC 57 | // update counters and packet type status 58 | #ifdef ENABLE_EXTENDED_WEBUI 59 | data.p1p2Cnt[P1P2_READ_OK]++; 60 | #endif /* ENABLE_EXTENDED_WEBUI */ 61 | if (setPacketStatus(RB[2], PACKET_SEEN, true) == true) { 62 | updateEeprom(); 63 | } 64 | // Send to UDP 65 | if (data.config.sendAllPackets || getPacketStatus(RB[2], PACKET_SENT) == true) { 66 | if (changedPacket(RB, n) == true) { 67 | // Send packets according to settings 68 | IPAddress remIp = data.config.remoteIp; 69 | if (data.config.udpBroadcast) remIp = { 255, 255, 255, 255 }; 70 | Udp.beginPacket(remIp, data.config.udpPort); 71 | Udp.write(RB, n); 72 | Udp.endPacket(); 73 | #ifdef ENABLE_EXTENDED_WEBUI 74 | data.udpCnt[UDP_SENT]++; 75 | #endif /* ENABLE_EXTENDED_WEBUI */ 76 | } 77 | } 78 | // Parse time and date 79 | if ((RB[0] == 0x00) && (RB[1] == 0x00) && (RB[2] == 0x12)) { 80 | if (date[1] == 23 && RB[5] == 0) { // midnight 81 | data.eepromDaikin.yesterday = data.eepromDaikin.today; 82 | data.eepromDaikin.today = 0; 83 | } 84 | for (byte i = 0; i < 6; i++) { 85 | date[i] = RB[i + 4]; 86 | } 87 | if (data.eepromDaikin.date[5] == 0) { 88 | memcpy(data.eepromDaikin.date, date, sizeof(data.eepromDaikin.date)); 89 | } 90 | if (data.statsDate[5] == 0) { 91 | memcpy(data.statsDate, date, sizeof(data.statsDate)); 92 | } 93 | } 94 | // Parse name 95 | if ((RB[0] == 0x40) && (RB[1] == 0x00) && (RB[2] == PACKET_TYPE_INDOOR_NAME)) { 96 | for (byte i = 0; i < NAME_SIZE - 1; i++) { 97 | if (RB[i + 4] == 0) break; 98 | daikinIndoor[i] = RB[i + 4]; 99 | } 100 | if (daikinIndoor[0] == '\0') daikinIndoor[0] = '-'; // if response from heat pup is empty, write '-' in order to prevent repeated requests from us 101 | } 102 | #ifdef ENABLE_EXTENDED_WEBUI 103 | if ((RB[0] == 0x40) && (RB[1] == 0x00) && (RB[2] == PACKET_TYPE_OUTDOOR_NAME)) { 104 | for (byte i = 0; i < NAME_SIZE - 1; i++) { 105 | if (RB[i + 4] == 0) break; 106 | daikinOutdoor[i] = RB[i + 4]; 107 | } 108 | if (daikinOutdoor[0] == '\0') daikinOutdoor[0] = '-'; // if response from heat pup is empty, write '-' in order to prevent repeated requests from us 109 | } 110 | #endif /* ENABLE_EXTENDED_WEBUI */ 111 | // check for other auxiliary controllers and get controller address 112 | if (((RB[1] & 0xF0) == 0xF0) && (RB[2] >= PACKET_TYPE_HANDSHAKE && RB[2] <= 0x3F)) { 113 | if (RB[0] == 0x00 && RB[2] == PACKET_TYPE_HANDSHAKE) { 114 | // 00Fx30 request message received 115 | // check if there is no other auxiliary controller 116 | if ((FxRequests[RB[1] & 0x0F]) == -1) { 117 | FxRequests[RB[1] & 0x0F] = 1; // skip 0 (reserved for "request not made") 118 | } else if ((FxRequests[RB[1] & 0x0F]) < F0THRESHOLD) { 119 | FxRequests[RB[1] & 0x0F]++; 120 | } else if ((FxRequests[RB[1] & 0x0F]) == F0THRESHOLD) { 121 | // Threshold reached, no auxiliary controller answering to address 0x(RB[1], HEX) 122 | if (controllerAddr == CONNECTING) { 123 | controllerAddr = RB[1]; 124 | } 125 | } 126 | } else if (RB[0] == 0x40) { 127 | // 40Fx3x auxiliary controller reply received - note this could be our own (slow, delta=F030DELAY or F03XDELAY) reply so only reset count if delta < min(F03XDELAY, F030DELAY) (- margin) 128 | // Note for developers using >1 P1P2Monitor-interfaces (=to self): this detection mechanism fails if there are 2 P1P2Monitor programs (and adapters) with same delay settings on the same bus. 129 | // check if there is any other auxiliary controller on 0x3x 130 | if ((delta < F03XDELAY - 2) && (delta < F030DELAY - 2)) { 131 | FxRequests[RB[1] & 0x0F] = -2; 132 | if (RB[1] == controllerAddr) { 133 | // controllerAddr conflicts with auxiliary controller 134 | // this should only happen if another auxiliary controller is connected after controllerAddr is set 135 | controllerAddr = DISCONNECTED; 136 | } 137 | } 138 | } 139 | } 140 | } 141 | 142 | /**************************************************************************/ 143 | /*! 144 | @brief Stores errors in counters. 145 | @param nread Bytes read. 146 | */ 147 | /**************************************************************************/ 148 | void processErrors(uint16_t nread) { 149 | uint8_t packetErrorFlags = 0; // 2-bit flag to store P1P2_WRITE_ERROR and P1P2_READ_ERROR 150 | 151 | for (uint16_t i = 0; i < nread; i++) { 152 | uint8_t errors = EB[i]; 153 | if (errors & (ERROR_SB // collision suspicion due to data verification error in reading back written data 154 | | ERROR_BE // collision suspicion due to data verification error in reading back written data 155 | | ERROR_BC)) { // collision suspicion due to 0 during 2nd half bit signal read back 156 | packetErrorFlags |= (1 << P1P2_WRITE_ERROR); 157 | } 158 | if (errors & (ERROR_PE // parity error detected 159 | | ERROR_OR // buffer overrun detected (overrun is after, not before, the read byte) 160 | | ERROR_CRC)) { // CRC error detected in readpacket 161 | packetErrorFlags |= (1 << P1P2_READ_ERROR); 162 | } 163 | } 164 | if (packetErrorFlags & (1 << P1P2_WRITE_ERROR)) data.p1p2Cnt[P1P2_WRITE_ERROR]++; 165 | if (packetErrorFlags & (1 << P1P2_READ_ERROR)) data.p1p2Cnt[P1P2_READ_ERROR]++; 166 | } 167 | 168 | /**************************************************************************/ 169 | /*! 170 | @brief Writes to the P1P2 bus. 171 | @param n Bytes to be written. 172 | */ 173 | /**************************************************************************/ 174 | void processWrite(uint16_t n) { 175 | //if the main controller sends request to our auxiliary controller, always respond 176 | WB[0] = 0x40; 177 | WB[1] = RB[1]; 178 | WB[2] = RB[2]; 179 | byte d = F03XDELAY; 180 | byte cmdType = 0; 181 | byte cmdLen = 0; 182 | if (cmdQueue.isEmpty() == false) { 183 | cmdLen = cmdQueue[0]; 184 | cmdType = cmdQueue[1]; 185 | } 186 | if (CRC_GEN) n--; // omit CRC from received-byte-counter 187 | if (n > WB_SIZE) { 188 | n = WB_SIZE; 189 | // Surprise: received 00Fx3x packet of size (nread) 190 | } 191 | for (byte i = 3; i < n; i++) WB[i] = 0xFF; // default response 192 | 193 | // Write command from queue 194 | if (cmdLen && RB[2] == cmdType) { // second byte in queue is packet type, compare to received packet type 195 | if ((cmdLen + 2U) <= n) { // check if param size in queue is not larger than space available in packet 196 | if (data.eepromDaikin.today < data.config.writeQuota) { 197 | for (byte i = 0; i < cmdLen; i++) { 198 | WB[i + 2] = cmdQueue[i + 1]; // skip the first byte in the queue (cmdLen) 199 | } 200 | data.eepromDaikin.total++; 201 | data.eepromDaikin.today++; 202 | // updateEeprom(); // is it really needed? 203 | } else { 204 | data.eepromDaikin.dropped++; 205 | } 206 | } else { 207 | data.eepromDaikin.invalid++; 208 | } 209 | updateEeprom(); // TODO is it really needed? Writes data to Arduino EEPROM whenever a command is written to the P1/P2 bus (& to the Daikin EEPROM) 210 | deleteCmd(); // delete cmd in Queue 211 | } else { 212 | switch (RB[2]) { 213 | case PACKET_TYPE_HANDSHAKE: // 0x30 214 | { 215 | d = F030DELAY; 216 | WB[3] = RB[3]; // trigger packet 0x31 if indicated in 00Fx30 request 217 | WB[4] = RB[4]; // trigger packet 0x32 if indicated in 00Fx30 request 218 | for (byte i = 5; i < n; i++) WB[i] = 0x00; // default response for the rest of the packet 219 | // 00F030 request message received, we will: 220 | // - reply with 40F030 response 221 | // - hijack time slot to send request counters 222 | if (cmdType >= PACKET_TYPE_CONTROL[FIRST] && cmdType <= PACKET_TYPE_CONTROL[LAST]) { 223 | // in: 17 byte; out: 17 byte; answer WB[7] should contain a 01 if we want to communicate a new setting in packet type 3X 224 | // set byte WB[7] to 0x01 for triggering F035 and byte WB[8] to 0x01 for triggering F036, etc. 225 | byte pos = (cmdType - PACKET_TYPE_HANDSHAKE) + 2; 226 | if (pos >= 3 && pos < n) WB[pos] = 0x01; 227 | } else if (!div2 && cmdLen > 0) { // some other command is in queue 228 | WB[0] = 0x00; 229 | WB[1] = 0x00; 230 | n = cmdLen + 2; 231 | if (n <= sizeof(WB)) { // check if size in queue is not larger than space available in packet 232 | for (byte i = 0; i < (cmdLen - 1); i++) { 233 | WB[i + 2] = cmdQueue[i + 1]; // skip the first byte in the queue (cmdLen) 234 | } 235 | } else { 236 | n = sizeof(WB); 237 | // TODO error 238 | } 239 | if (cmdType == PACKET_TYPE_COUNTER) { 240 | if (cmdQueue[2] < 5) { 241 | cmdQueue.push(cmdLen); 242 | cmdQueue.push(cmdType); 243 | cmdQueue.push(cmdQueue[2] + 1); 244 | } 245 | } 246 | div2 = 2; 247 | deleteCmd(); // delete cmd in Queue 248 | } 249 | if (div2) { // insert counterRequest messages and other commands at end of each 2nd cycle 250 | div2--; 251 | } 252 | } 253 | break; 254 | case 0x31: // in: 15 byte; out: 15 byte; out pattern is copy of in pattern except for 2 bytes RB[7] RB[8]; function partly date/time, partly unknown 255 | // RB[7] RB[8] seem to identify the auxiliary controller type; 256 | // Do pretend to be a LAN adapter (even though this may trigger "data not in sync" upon restart?) 257 | // If we don't set address, installer mode in main thermostat may become inaccessible 258 | for (byte i = 3; i < n; i++) WB[i] = RB[i]; 259 | WB[7] = CTRL_ID[0]; 260 | WB[8] = CTRL_ID[1]; 261 | break; 262 | case 0x32: // in: 19 byte: out 19 byte, out is copy in 263 | for (byte i = 3; i < n; i++) WB[i] = RB[i]; 264 | break; 265 | case 0x33: // not seen; reply with FF 266 | case 0x34: // not seen; reply with FF 267 | case 0x35: // in: 21 byte; out 21 byte; 3-byte parameters reply with FF 268 | case 0x36: // in: 23 byte; out 23 byte; 2-byte parameters; reply with FF 269 | case 0x37: // in: 23 byte; out 23 byte; 3-byte parameters; reply with FF 270 | case 0x38: // in: 21 byte; out 21 byte; 4-byte parameters; reply with FF 271 | case 0x39: // in: 21 byte; out 21 byte; 4-byte parameters; reply with FF 272 | case 0x3A: // in: 21 byte; out 21 byte; 1-byte parameters reply with FF 273 | case 0x3B: // in: 23 byte; out 23 byte; 2-byte parameters; reply with FF 274 | case 0x3C: // in: 23 byte; out 23 byte; 3-byte parameters; reply with FF 275 | case 0x3D: // in: 21 byte; out: 21 byte; 4-byte parameters; reply with FF 276 | break; 277 | case 0x3E: // schedule related packet 278 | // 0x3E01, 0x3E02, ... in: 23 byte; out: 23 byte; out 40F13E01(even for higher) + 19xFF 279 | WB[3] = RB[3]; 280 | break; 281 | default: // not seen, reply with FF 282 | break; 283 | } 284 | } 285 | P1P2Serial.writepacket(WB, n, d, CRC_GEN, CRC_FEED); 286 | #ifdef ENABLE_EXTENDED_WEBUI 287 | data.p1p2Cnt[P1P2_WRITE_OK]++; 288 | #endif /* ENABLE_EXTENDED_WEBUI */ 289 | } 290 | 291 | /**************************************************************************/ 292 | /*! 293 | @brief Checks whether the packet payload (received via P1P2) has changed 294 | and stores new value. 295 | @param packet Packet payload 296 | @param packetLen Packet length 297 | @return True if a packet is observed for the first time or if any byte 298 | in the payload has changed. 299 | */ 300 | /**************************************************************************/ 301 | bool changedPacket(byte packet[], const byte packetLen) { 302 | bool newPacket = false; 303 | byte pts = (packet[0] >> 6) & 0x01; 304 | byte pti = packet[2] - PACKET_TYPE_DATA[FIRST]; 305 | byte bytestart = 0; 306 | for (byte i = 0; i <= pts; i++) { 307 | for (byte j = 0; j < (PACKET_TYPE_DATA[LAST] - PACKET_TYPE_DATA[FIRST] + 1); j++) { 308 | if (i == pts && j == pti) break; 309 | bytestart += PACKET_PAYLOAD_SIZE[i][j]; 310 | } 311 | } 312 | if (packet[2] < PACKET_TYPE_DATA[FIRST] || packet[2] > PACKET_TYPE_DATA[LAST] || data.config.sendDataPackets == DATA_ALWAYS) { 313 | newPacket = true; 314 | } else if (packetLen - 3 > PACKET_PAYLOAD_SIZE[pts][pti]) { 315 | // Warning: packet longer than expected 316 | newPacket = true; 317 | } else { 318 | for (byte i = 0; i < packetLen - 3; i++) { 319 | byte pi2 = bytestart + i; 320 | if (pi2 >= SAVED_PACKETS_SIZE) { 321 | pi2 = 0; 322 | // Warning: pi2 > SAVED_PACKETS_SIZE 323 | return 0; 324 | } 325 | // this byte or at least some bits have been seen and saved before. 326 | if (savedPackets[pi2] != packet[i + 3]) { 327 | newPacket = true; 328 | savedPackets[pi2] = packet[i + 3]; 329 | } 330 | } 331 | } 332 | return newPacket; 333 | } 334 | 335 | /**************************************************************************/ 336 | /*! 337 | @brief Checks whether the parameter 36 value in the command (received 338 | via UDP or web interface) changed (more than hysteresis), new value is saved. 339 | @param cmd Command received via UDP or web interface 340 | @return True if a parameter is received from UDP for the first time 341 | or if change in param value is greater than hysteresis. 342 | */ 343 | /**************************************************************************/ 344 | bool changed36Param(byte cmd[]) { 345 | bool newVal = false; 346 | if (cmd[0] != 0x36) { 347 | newVal = true; 348 | } else if (cmd[2] != 0 || cmd[1] > MAX_36_PARAMS) { 349 | // TODO Warning: param number is higher than max allowed 350 | newVal = false; 351 | } else { 352 | byte paramNum = cmd[1]; 353 | int16_t paramVal = (cmd[4] << 8) | cmd[3]; 354 | int16_t storedVal = (saved36Params[paramNum][1] << 8) | saved36Params[paramNum][0]; 355 | // this byte or at least some bits have been seen and saved before. 356 | if (abs(int16_t(storedVal - paramVal)) >= int16_t(data.config.hysteresis)) { 357 | newVal = true; 358 | saved36Params[paramNum][0] = cmd[3]; 359 | saved36Params[paramNum][1] = cmd[4]; 360 | } 361 | } 362 | return newVal; 363 | } -------------------------------------------------------------------------------- /arduino-altherma-controller/04-webserver.ino: -------------------------------------------------------------------------------- 1 | const byte URI_SIZE = 24; // a smaller buffer for uri 2 | const byte POST_SIZE = 24; // a smaller buffer for single post parameter + key 3 | 4 | // Actions that need to be taken after saving configuration. 5 | enum action_type : byte { 6 | ACT_NONE, 7 | ACT_DEFAULT, // Load default factory settings (but keep MAC address) 8 | ACT_MAC, // Generate new random MAC 9 | ACT_REBOOT, // Reboot the microcontroller 10 | ACT_RESET_ETH, // Ethernet reset 11 | ACT_RESET_EEPROM, // Reset Daikin EEPROM Writes counter 12 | ACT_RESET_STATS, // Reset P1P2 Read Statistics 13 | ACT_CONNECT, // Connect Controller 14 | ACT_DISCONNECT, // Disconnect Controller 15 | ACT_CLEAR_QUOTA, // Clear Daikin EEPROM Writes Daily Quota 16 | ACT_WEB // Restart webserver 17 | }; 18 | enum action_type action; 19 | 20 | // Pages served by the webserver. Order of elements defines the order in the left menu of the web UI. 21 | // URL of the page (*.htm) contains number corresponding to its position in this array. 22 | // The following enum array can have a maximum of 10 elements (incl. PAGE_NONE and PAGE_WAIT) 23 | enum page : byte { 24 | PAGE_ERROR, // 404 Error 25 | PAGE_INFO, 26 | PAGE_STATUS, 27 | PAGE_IP, 28 | PAGE_TCP, 29 | PAGE_P1P2, 30 | PAGE_FILTER, 31 | PAGE_TOOLS, 32 | PAGE_WAIT, // page with "Reloading. Please wait..." message. 33 | PAGE_DATA, // d.json 34 | }; 35 | 36 | // Keys for POST parameters, used in web forms and processed by processPost() function. 37 | // Using enum ensures unique identification of each POST parameter key and consistence across functions. 38 | // In HTML code, each element will apear as number corresponding to its position in this array. 39 | enum post_key : byte { 40 | POST_NONE, // reserved for NULL 41 | POST_DHCP, // enable DHCP 42 | POST_MAC, 43 | POST_MAC_1, 44 | POST_MAC_2, 45 | POST_MAC_3, 46 | POST_MAC_4, 47 | POST_MAC_5, 48 | POST_IP, 49 | POST_IP_1, 50 | POST_IP_2, 51 | POST_IP_3, // IP address || Each part of an IP address has its own POST parameter. || 52 | POST_SUBNET, 53 | POST_SUBNET_1, 54 | POST_SUBNET_2, 55 | POST_SUBNET_3, // subnet || Because HTML code for IP, subnet, gateway and DNS || 56 | POST_GATEWAY, 57 | POST_GATEWAY_1, 58 | POST_GATEWAY_2, 59 | POST_GATEWAY_3, // gateway || is generated through one (nested) for-loop, || 60 | POST_DNS, 61 | POST_DNS_1, 62 | POST_DNS_2, 63 | POST_DNS_3, // DNS || all these 16 enum elements must be listed in succession!! || 64 | POST_UDP_BROADCAST, 65 | POST_REM_IP, 66 | POST_REM_IP_1, 67 | POST_REM_IP_2, 68 | POST_REM_IP_3, // remote IP 69 | POST_UDP, // local UDP port 70 | POST_WEB, // web UI port 71 | POST_CONTROL_MODE, // controller mode 72 | POST_SEND_ALL, // send all packets 73 | POST_COUNTER_PERIOD, // period for counter requests 74 | POST_DATA_PACKETS, // save data packets (send only if payload changed) 75 | POST_TIMEOUT, // connection timeout 76 | POST_QUOTA, // write throttle 77 | POST_HYSTERESIS, // temp setpoint hysteresis 78 | POST_CMD_TYPE, // write command packet type 79 | POST_CMD_PARAM_1, // write command parameter number 80 | POST_CMD_PARAM_2, // write command parameter number 81 | POST_CMD_VAL_1, // write command parameter value 82 | POST_CMD_VAL_2, 83 | POST_CMD_VAL_3, 84 | POST_CMD_VAL_4, 85 | POST_ACTION, // actions on Tools page 86 | }; 87 | 88 | 89 | 90 | // Keys for JSON elements, used in: 1) JSON documents, 2) ID of span tags, 3) Javascript. 91 | enum JSON_type : byte { 92 | JSON_RUNTIME, // Runtime 93 | JSON_DAIKIN_INDOOR, // Daikin Indoor Unit 94 | JSON_DAIKIN_OUTDOOR, // Daikin Outdoor Unit 95 | JSON_DATE, // date and time 96 | JSON_DAIKIN_EEPROM_DATE, // EEPROM Stats since 97 | JSON_DAIKIN_EEPROM, // EEPROM Health 98 | JSON_WRITE_P1P2, // write P1P2 button 99 | JSON_P1P2_STATS_DATE, // P1P2 Stats since 100 | JSON_P1P2_STATS, // Multiple P1P2 Read Statistics 101 | JSON_UDP_STATS, // Multiple P1P2 Write Statistics 102 | JSON_CONTROLLER, // Controller Mode 103 | JSON_OTHER_CONTROLLERS, // Other controllers connected 104 | JSON_LAST, // Must be the very last element in this array 105 | }; 106 | 107 | /**************************************************************************/ 108 | /*! 109 | @brief Receives GET requests for web pages, receives POST data from web forms, 110 | calls @ref processPost() function, sends web pages. For simplicity, all web pages 111 | should are numbered (1.htm, 2.htm, ...), the page number is passed to 112 | the @ref sendPage() function. Also executes actions (such as ethernet restart, 113 | reboot) during "please wait" web page. 114 | @param client Ethernet TCP client. 115 | */ 116 | /**************************************************************************/ 117 | void recvWeb(EthernetClient &client) { 118 | char uri[URI_SIZE]; // the requested page 119 | memset(uri, 0, sizeof(uri)); 120 | while (client.available()) { // start reading the first line which should look like: GET /uri HTTP/1.1 121 | if (client.read() == ' ') break; // find space before /uri 122 | } 123 | byte len = 0; 124 | while (client.available() && len < sizeof(uri) - 1) { 125 | char c = client.read(); // parse uri 126 | if (c == ' ') break; // find space after /uri 127 | uri[len] = c; 128 | len++; 129 | } 130 | while (client.available()) { 131 | if (client.read() == '\r') 132 | if (client.read() == '\n') 133 | if (client.read() == '\r') 134 | if (client.read() == '\n') 135 | break; // find 2 end of lines between header and body 136 | } 137 | if (client.available()) { 138 | processPost(client); // parse post parameters 139 | } 140 | 141 | // Get the requested page from URI 142 | byte reqPage = PAGE_ERROR; // requested page, 404 error is a default 143 | if (uri[0] == '/') { 144 | if (uri[1] == '\0') { 145 | reqPage = PAGE_INFO; // Homepage 146 | } else if (uri[1] >= '0' && uri[1] <= '9' && strcmp(uri + 2, ".htm") == 0) { 147 | reqPage = byte(uri[1] - '0'); // Convert ASCII digit to byte 148 | if (reqPage > PAGE_WAIT) reqPage = PAGE_ERROR; 149 | } else if (strcmp(uri, "/d.json") == 0) { 150 | reqPage = PAGE_DATA; 151 | } 152 | } 153 | // Actions that require "please wait" page 154 | if (action == ACT_WEB || action == ACT_MAC || action == ACT_RESET_ETH || action == ACT_REBOOT || action == ACT_DEFAULT) { 155 | reqPage = PAGE_WAIT; 156 | } 157 | // Send page 158 | sendPage(client, reqPage); 159 | 160 | // Do all actions before the "please wait" redirects (5s delay at the moment) 161 | if (reqPage == PAGE_WAIT) { 162 | delay(500); // wait for the wait page to load 163 | switch (action) { 164 | case ACT_WEB: 165 | case ACT_MAC: 166 | case ACT_RESET_ETH: 167 | for (byte s = 0; s < maxSockNum; s++) { 168 | // close all TCP and UDP sockets 169 | disconSocket(s); 170 | } 171 | startEthernet(); 172 | break; 173 | case ACT_REBOOT: 174 | case ACT_DEFAULT: 175 | resetFunc(); 176 | break; 177 | default: 178 | break; 179 | } 180 | } 181 | action = ACT_NONE; 182 | } 183 | 184 | /**************************************************************************/ 185 | /*! 186 | @brief Processes POST data from forms and buttons, updates data.config (in RAM) 187 | and saves config into EEPROM. Executes actions which do not require webserver restart 188 | @param client Ethernet TCP client. 189 | */ 190 | /**************************************************************************/ 191 | void processPost(EthernetClient &client) { 192 | byte command[1 + 2 + MAX_PARAM_SIZE]; // 1 byte packet type + 2 bytes param number + MAX_PARAM_SIZE bytes param value 193 | byte cmdLen = 0; // Length of the P1P2 command from WebUI 194 | while (client.available()) { 195 | char post[POST_SIZE]; 196 | byte len = 0; 197 | bool isDecimal = false; 198 | while (client.available() && len < sizeof(post) - 1) { 199 | char c = client.read(); 200 | if (c == '&') break; 201 | if (c == ',' || c == '.') { 202 | isDecimal = true; 203 | continue; 204 | } 205 | post[len] = c; 206 | len++; 207 | } 208 | post[len] = '\0'; 209 | char *paramKey = post; 210 | char *paramValue = post; 211 | while (*paramValue) { 212 | if (*paramValue == '=') { 213 | paramValue++; 214 | break; 215 | } 216 | paramValue++; 217 | } 218 | if (*paramValue == '\0') 219 | continue; // do not process POST parameter if there is no parameter value 220 | byte paramKeyByte = strToByte(paramKey); 221 | uint16_t paramValueUint = atol(paramValue); 222 | if (paramKey[0] == 'p') { // POST parameter starts with 'p': this is a setting for sent packets 223 | setPacketStatus(strToByte(paramKey + 1), PACKET_SENT, byte(paramValueUint)); 224 | continue; 225 | } 226 | 227 | switch (paramKeyByte) { 228 | case POST_NONE: // reserved, because atoi / atol returns NULL in case of error 229 | break; 230 | #ifdef ENABLE_DHCP 231 | case POST_DHCP: 232 | { 233 | data.config.enableDhcp = byte(paramValueUint); 234 | } 235 | break; 236 | case POST_DNS ... POST_DNS_3: 237 | { 238 | data.config.dns[paramKeyByte - POST_DNS] = byte(paramValueUint); 239 | } 240 | break; 241 | #endif /* ENABLE_DHCP */ 242 | case POST_CMD_TYPE: 243 | { 244 | command[0] = strToByte(paramValue); 245 | } 246 | break; 247 | case POST_CMD_PARAM_1: 248 | { 249 | command[1] = strToByte(paramValue); 250 | } 251 | break; 252 | case POST_CMD_PARAM_2: 253 | { 254 | command[2] = strToByte(paramValue); 255 | } 256 | break; 257 | case POST_CMD_VAL_1 ... POST_CMD_VAL_4: 258 | { 259 | cmdLen = 3 + paramKeyByte - POST_CMD_VAL_1 + 1; 260 | command[cmdLen - 1] = strToByte(paramValue); 261 | } 262 | break; 263 | case POST_MAC ... POST_MAC_5: 264 | { 265 | action = ACT_RESET_ETH; // this RESET_ETH is triggered when the user changes anything on the "IP Settings" page. 266 | // No need to trigger RESET_ETH for other cases (POST_SUBNET, POST_GATEWAY etc.) 267 | // if "Randomize" button is pressed, action is set to ACT_MAC 268 | data.mac[paramKeyByte - POST_MAC] = strToByte(paramValue); 269 | } 270 | break; 271 | case POST_IP ... POST_IP_3: 272 | { 273 | data.config.ip[paramKeyByte - POST_IP] = byte(paramValueUint); 274 | } 275 | break; 276 | case POST_SUBNET ... POST_SUBNET_3: 277 | { 278 | data.config.subnet[paramKeyByte - POST_SUBNET] = byte(paramValueUint); 279 | } 280 | break; 281 | case POST_GATEWAY ... POST_GATEWAY_3: 282 | { 283 | data.config.gateway[paramKeyByte - POST_GATEWAY] = byte(paramValueUint); 284 | } 285 | break; 286 | case POST_REM_IP ... POST_REM_IP_3: 287 | { 288 | data.config.remoteIp[paramKeyByte - POST_REM_IP] = byte(paramValueUint); 289 | } 290 | break; 291 | case POST_UDP_BROADCAST: 292 | data.config.udpBroadcast = byte(paramValueUint); 293 | break; 294 | case POST_UDP: 295 | { 296 | if (data.config.udpPort != paramValueUint) { 297 | data.config.udpPort = paramValueUint; 298 | Udp.stop(); 299 | Udp.begin(data.config.udpPort); 300 | } 301 | } 302 | break; 303 | case POST_WEB: 304 | { 305 | if (paramValueUint != data.config.webPort) { // continue only if the value changed 306 | data.config.webPort = paramValueUint; 307 | action = ACT_WEB; 308 | } 309 | } 310 | break; 311 | case POST_CONTROL_MODE: 312 | data.config.controllerMode = byte(paramValueUint); 313 | break; 314 | case POST_TIMEOUT: 315 | data.config.connectTimeout = byte(paramValueUint); 316 | break; 317 | case POST_QUOTA: 318 | data.config.writeQuota = byte(paramValueUint); 319 | break; 320 | case POST_HYSTERESIS: 321 | if (isDecimal == false) paramValueUint *= 10; 322 | data.config.hysteresis = byte(paramValueUint); 323 | break; 324 | case POST_SEND_ALL: 325 | data.config.sendAllPackets = byte(paramValueUint); 326 | memset(savedPackets, 0xFF, sizeof(savedPackets)); // reset saved packets whenever some setting on "Packet Filter" page changes 327 | break; 328 | case POST_COUNTER_PERIOD: 329 | data.config.counterPeriod = byte(paramValueUint); 330 | break; 331 | case POST_DATA_PACKETS: 332 | data.config.sendDataPackets = byte(paramValueUint); 333 | break; 334 | case POST_ACTION: 335 | action = action_type(paramValueUint); 336 | break; 337 | default: 338 | break; 339 | } 340 | } // while (point != NULL) 341 | switch (action) { 342 | case ACT_DEFAULT: 343 | { 344 | data.config = DEFAULT_CONFIG; 345 | setPacketStatus(PACKET_TYPE_COUNTER, PACKET_SENT, true); 346 | for (byte i = PACKET_TYPE_DATA[FIRST]; i <= PACKET_TYPE_DATA[LAST]; i++) { 347 | setPacketStatus(i, PACKET_SENT, true); 348 | } 349 | break; 350 | } 351 | case ACT_MAC: 352 | generateMac(); 353 | break; 354 | case ACT_RESET_STATS: 355 | resetStats(); 356 | break; 357 | case ACT_RESET_EEPROM: 358 | resetEepromStats(); 359 | break; 360 | case ACT_CONNECT: 361 | controllerAddr = CONNECTING; 362 | break; 363 | case ACT_DISCONNECT: 364 | controllerAddr = DISCONNECTED; 365 | break; 366 | case ACT_CLEAR_QUOTA: 367 | data.eepromDaikin.today = 0; 368 | data.eepromDaikin.dropped = 0; 369 | data.eepromDaikin.invalid = 0; 370 | break; 371 | default: 372 | break; 373 | } 374 | // if new P1P2 command received, put into queue 375 | if (cmdLen > 1) { 376 | checkCommand(command, cmdLen); 377 | return; // do not update EEPROM 378 | } 379 | if (action == ACT_CONNECT || action == ACT_DISCONNECT) return; // do not update EEPROM 380 | // new parameter values received, save them to EEPROM (but do not save after Connect or Disconnect button or after manual P1P2 write) 381 | updateEeprom(); // it is safe to call, only changed values (and changed error and data counters) are updated 382 | } 383 | 384 | /**************************************************************************/ 385 | /*! 386 | @brief Parses string and returns single byte. 387 | @param myStr String (2 chars, 1 char + null or 1 null) to be parsed. 388 | @return Parsed byte. 389 | */ 390 | /**************************************************************************/ 391 | byte strToByte(const char myStr[]) { 392 | if (!myStr) return 0; 393 | byte x = 0; 394 | for (byte i = 0; i < 2; i++) { 395 | char c = myStr[i]; 396 | if (c >= '0' && c <= '9') { 397 | x *= 16; 398 | x += c - '0'; 399 | } else if (c >= 'A' && c <= 'F') { 400 | x *= 16; 401 | x += (c - 'A') + 10; 402 | } else if (c >= 'a' && c <= 'f') { 403 | x *= 16; 404 | x += (c - 'a') + 10; 405 | } 406 | } 407 | return x; 408 | } 409 | 410 | char __printbuffer[3]; 411 | /**************************************************************************/ 412 | /*! 413 | @brief Converts byte to char string, from https://github.com/RobTillaart/printHelpers 414 | @param val Byte to be conferted. 415 | @return Char string. 416 | */ 417 | /**************************************************************************/ 418 | char *hex(byte val) { 419 | char *buffer = __printbuffer; 420 | byte digits = 2; 421 | buffer[digits] = '\0'; 422 | while (digits > 0) { 423 | byte v = val & 0x0F; 424 | val >>= 4; 425 | digits--; 426 | buffer[digits] = (v < 10) ? '0' + v : ('A' - 10) + v; 427 | } 428 | return buffer; 429 | } 430 | 431 | /**************************************************************************/ 432 | /*! 433 | @brief Converts date to number of days since 1.1.2000. 434 | @param date Date 435 | @return Number of days since 1.1.2000 436 | */ 437 | /**************************************************************************/ 438 | uint16_t days(byte *date) { 439 | return (date[3] * 365) + ((date[4] - 1) * 30) + date[5]; 440 | } -------------------------------------------------------------------------------- /arduino-altherma-controller/advanced_settings.h: -------------------------------------------------------------------------------- 1 | /* Advanced settings, extra functions and default config 2 | */ 3 | 4 | /****** FUNCTIONALITY ******/ 5 | 6 | // #define ENABLE_EXTENDED_WEBUI // Enable extended Web UI (additional items and settings), consumes FLASH memory 7 | // uncomment ENABLE_EXTENDED_WEBUI if you have a board with large FLASH memory (Arduino Mega) 8 | 9 | // #define ENABLE_DHCP // Enable DHCP (Auto IP settings), consumes a lot of FLASH memory 10 | 11 | #if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560) 12 | #define ENABLE_EXTENDED_WEBUI 13 | #define ENABLE_DHCP 14 | #endif 15 | 16 | /****** DEFAULT CONFIGURATION ******/ 17 | /* 18 | Arduino loads user settings stored in EEPROM, even if you flash new program to it. 19 | 20 | Arduino loads factory defaults if: 21 | 1) User clicks "Load default settings" in WebUI (factory reset configuration, keeps MAC) 22 | 2) VERSION_MAJOR changes (factory reset configuration AND generates new MAC) 23 | */ 24 | 25 | /****** IP Settings ******/ 26 | const bool DEFAULT_AUTO_IP = false; // Default Auto IP setting (only used if ENABLE_DHCP) 27 | #define DEFAULT_STATIC_IP \ 28 | { 192, 168, 1, 254 } // Default Static IP 29 | #define DEFAULT_SUBMASK \ 30 | { 255, 255, 255, 0 } // Default Submask 31 | #define DEFAULT_GATEWAY \ 32 | { 192, 168, 1, 1 } // Default Gateway 33 | #define DEFAULT_DNS \ 34 | { 192, 168, 1, 1 } // Default DNS Server (only used if ENABLE_DHCP) 35 | 36 | /****** TCP/UDP Settings ******/ 37 | #define DEFAULT_REMOTE_IP \ 38 | { 192, 168, 1, 22 } // Default Remote IP (only used if ENABLE_EXTENDED_WEBUI) 39 | const bool DEFAULT_BROADCAST = true; // Default UDP Broadcast setting (Send and Receive UDP) 40 | const uint16_t DEFAULT_UDP_PORT = 10000; // Default UDP Port 41 | const uint16_t DEFAULT_WEB_PORT = 80; // Default WebUI Port 42 | 43 | /****** P1P2 Settings ******/ 44 | const byte DEFAULT_COTROLLER_MODE = CONTROL_MANUAL; // Default Controller Mode (CONTROL_MANUAL or CONTROL_AUTO) 45 | const byte DEFAULT_EEPROM_QUOTA = 24; // Default EEPROM Write Quota 46 | const byte DEFAUT_TEMPERATURE_HYSTERESIS = 10; // Default Target Temperature Hysteresis in 1/10 °C 47 | 48 | /****** Packet Filter ******/ 49 | const bool DEFAULT_SEND_ALL = false; // Default Send All Packet Types 50 | const byte DEFAULT_COUNTER_PERIOD = 10; // Default Counters Packet Request Period 51 | const byte DEFAULT_DATA_PACKETS_MODE = DATA_CHANGE_AND_REQUEST; // Default Data Packets Mode (DATA_ALWAYS, DATA_CHANGE_AND_REQUEST or DATA_ONLY_CHANGE) 52 | 53 | 54 | /****** ADVANCED SETTINGS ******/ 55 | 56 | const byte MAX_QUEUE_DATA = 64; // total length of UDP commands stored in a queue (in bytes) 57 | const byte PACKET_TYPE_DATA[2] = { 0x10, 0x16 }; // First and last data packet type, regularly sent between heat pump and main controller 58 | const byte PACKET_TYPE_CONTROL[2] = { 0x30, 0x3E }; // First and last control packet type, between main and auxiliary controller 59 | const byte PACKET_TYPE_INDOOR_NAME = 0xB1; // Heat pump indoorname packet type 60 | const byte PACKET_TYPE_OUTDOOR_NAME = 0xA1; // Heat pump outdoor name packet type 61 | const byte PACKET_TYPE_COUNTER = 0xB8; // Counters packet type 62 | const byte F030DELAY = 100; // Time delay for in ms auxiliary controller simulation, should be larger than any response of other auxiliary controllers (which is typically 25-80 ms) 63 | const byte F03XDELAY = 50; // Time delay for in ms auxiliary controller simulation, should preferably be a bit larger than any regular response from auxiliary controllers (which is typically 25 ms) 64 | const byte F0THRESHOLD = 5; // Number of 00Fx30 messages to remain unanswered before we feel safe to act as auxiliary controller 65 | // Each message takes ~770ms so we can use F0THRESHOLD to set minimum and default connectTimeout 66 | 67 | const byte DATA_PACKETS_CNT = PACKET_TYPE_DATA[LAST] - PACKET_TYPE_DATA[FIRST] + 1; 68 | const byte CTRL_PACKETS_CNT = PACKET_TYPE_CONTROL[LAST] - PACKET_TYPE_CONTROL[FIRST] + 1; 69 | //byte packetsrc = { { 00 }, { 40 } }; 70 | //byte packettype = { { 10,11, 12,13, 14,15, 16 }, { 10, 11, 12, 13, 14,15,16 } }; 71 | const byte PACKET_PAYLOAD_SIZE[2][DATA_PACKETS_CNT] = { { 20, 8, 15, 3, 15, 6, 16 }, { 20, 20, 20, 16, 19, 9, 9 } }; // sum is SAVED_PACKETS_SIZE = 196 72 | const byte SAVED_PACKETS_SIZE = 196; // if SAVED_PACKETS_SIZE > 256, change datatype for these variables: SAVED_PACKETS_SIZE, bytestart, pi2 73 | //byte packettype = {30,31,32,33,34,35,36,37,38,39,3A,3B,3C,3D,3E } 74 | const byte PACKET_PARAM_VAL_SIZE[CTRL_PACKETS_CNT] = { 0, 0, 0, 0, 0, 1, 2, 3, 4, 4, 1, 2, 3, 4, 0 }; // 0 = write command not supported (yet) 75 | const byte MAX_PARAM_SIZE = 6; 76 | const byte MAX_36_PARAMS = 0x0D; // max number of packet type 0x36 parameters stored 77 | 78 | // CRC settings 79 | const byte CRC_GEN = 0xD9; // Default generator/Feed for CRC check; these values work at least for the Daikin hybrid 80 | const byte CRC_FEED = 0x00; // Define CRC_GEN to 0x00 means no CRC is checked when reading or added when writing 81 | 82 | const byte WB_SIZE = 32; // P1/P2 write buffer size for writing to P1P2bus, max packet size is 32 (have not seen anytyhing over 24 (23+CRC)) 83 | const byte RB_SIZE = 33; // P1/P2 read buffer size to store raw data and error codes read from P1P2bus; 1 extra for reading back CRC byte; 24 might be enough 84 | const uint16_t INIT_SDTO = 2500; // P1/P2 write time-out delay (ms) 85 | 86 | const byte CTRL_ID[] = { 0xB4, 0x10 }; // LAN adapter ID in 0x31 payload bytes 7 and 8 87 | 88 | const byte MAC_START[3] = { 0x90, 0xA2, 0xDA }; // MAC range for Gheo SA 89 | #define ETH_RESET_PIN 7 // Ethernet shield reset pin (deals with power on reset issue on low quality ethernet shields) 90 | const uint16_t ETH_RESET_DELAY = 500; // Delay (ms) during Ethernet start, wait for Ethernet shield to start (reset issue on low quality ethernet shields) 91 | const uint16_t WEB_IDLE_TIMEOUT = 400; // Time (ms) from last client data after which webserver TCP socket could be disconnected, non-blocking. 92 | const uint16_t TCP_DISCON_TIMEOUT = 500; // Timeout (ms) for client DISCON socket command, non-blocking alternative to https://www.arduino.cc/reference/en/libraries/ethernet/client.setconnectiontimeout/ 93 | const uint16_t TCP_RETRANSMISSION_TIMEOUT = 50; // Ethernet controller’s timeout (ms), blocking (see https://www.arduino.cc/reference/en/libraries/ethernet/ethernet.setretransmissiontimeout/) 94 | const byte TCP_RETRANSMISSION_COUNT = 3; // Number of transmission attempts the Ethernet controller will make before giving up (see https://www.arduino.cc/reference/en/libraries/ethernet/ethernet.setretransmissioncount/) 95 | const uint16_t FETCH_INTERVAL = 2000; // Fetch API interval (ms) for the Modbus Status webpage to renew data from JSON served by Arduino 96 | 97 | const byte DATA_START = 96; // Start address where config and counters are saved in EEPROM 98 | const byte EEPROM_INTERVAL = 6; // Interval (hours) for saving Modbus statistics to EEPROM (in order to minimize writes to EEPROM) 99 | -------------------------------------------------------------------------------- /arduino-altherma-controller/arduino-altherma-controller.ino: -------------------------------------------------------------------------------- 1 | /* Altherma UDP Controller: Monitors and controls Daikin E-Series (Altherma) heat pumps through P1/P2 bus. 2 | 3 | Version history 4 | v0.1 2020-11-30 Initial commit, save history of selected packets, settings 5 | v0.2 2020-12-05 Hysteresis, vertify commands sent to P1P2 6 | v0.3 2020-12-09 More effective and reliable writing to P1P2 bus 7 | v0.4 2020-12-10 Minor tweaks 8 | v1.0 2023-04-18 Major upgrade: web interface, store settings in EEPROM, P1P2 error counters 9 | v2.0 2023-08-25 Manual MAC, Daikin EEPROM write daily quota, Simplify Arduino EEPROM read / write, Tools page 10 | v2.1 2023-09-17 Improve advanced settings, disable DHCP renewal fallback 11 | v3.0 2024-02-02 Function comments. Remove "Disabled" Controller mode (only Manual; Auto), 12 | improved automatic connection to the P1P2 bus, connect to any peripheral address 13 | between 0xF0 to 0xFF (depends on Altherma model), show other controllers and available addresses. 14 | v4.0 2025-03-09 CSS improvement, code optimization (with some help from ChatGPT), simplify P1P2 Status page, 15 | target temp. hysteresis in decimals, fix 404 error page, bugfix 0x30 packet, 16 | more virtual outputs in Loxone Config, rename some inputs in Loxone Config 17 | */ 18 | 19 | const byte VERSION[] = { 4, 0 }; 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include // CircularBuffer https://github.com/rlogiacco/CircularBuffer 26 | #include 27 | #include // StreamLib https://github.com/jandrassy/StreamLib 28 | // #include // P1P2Serial https://github.com/Arnold-n/P1P2Serial 29 | #include "src/P1P2Serial_mod/P1P2Serial_mod.h" // modified P1P2Serial library 30 | 31 | // these are used by CreateTrulyRandomSeed() function 32 | #include 33 | #include 34 | #include 35 | 36 | enum first_last_t : byte { 37 | FIRST, 38 | LAST 39 | }; 40 | 41 | enum packetStatus_t : byte { 42 | PACKET_SEEN, // Packet Type was detected on P1P2 bus 43 | PACKET_SENT, // Packet is sent via UDP 44 | PACKET_LAST // Number of status flags in this enum. Must be the last element within this enum!! 45 | }; 46 | 47 | enum mode_t : byte { 48 | CONTROL_MANUAL, // Manual Connect 49 | CONTROL_AUTO // Auto Connect 50 | }; 51 | 52 | // Data Packets 53 | enum data_packets_t : byte { 54 | DATA_ALWAYS, // Always Send (~770ms cycle) 55 | DATA_CHANGE_AND_REQUEST, // If Payload Changed & At Counter Requests 56 | DATA_ONLY_CHANGE // Only If Payload Changed 57 | }; 58 | 59 | #include "advanced_settings.h" 60 | 61 | typedef struct { 62 | byte ip[4]; 63 | byte subnet[4]; 64 | byte gateway[4]; 65 | #ifdef ENABLE_DHCP 66 | byte dns[4]; // only used if ENABLE_DHCP 67 | bool enableDhcp; // only used if ENABLE_DHCP 68 | #endif 69 | byte remoteIp[4]; 70 | bool udpBroadcast; 71 | uint16_t udpPort; 72 | uint16_t webPort; 73 | byte controllerMode; 74 | byte connectTimeout; 75 | byte hysteresis; 76 | byte writeQuota; 77 | bool sendAllPackets; 78 | byte counterPeriod; 79 | byte sendDataPackets; 80 | byte packetStatus[PACKET_LAST][256 / 8]; 81 | } config_t; 82 | 83 | // default values for the web UI 84 | const config_t DEFAULT_CONFIG = { 85 | DEFAULT_STATIC_IP, 86 | DEFAULT_SUBMASK, 87 | DEFAULT_GATEWAY, 88 | #ifdef ENABLE_DHCP 89 | DEFAULT_DNS, 90 | DEFAULT_AUTO_IP, 91 | #endif 92 | DEFAULT_REMOTE_IP, 93 | DEFAULT_BROADCAST, 94 | DEFAULT_UDP_PORT, 95 | DEFAULT_WEB_PORT, 96 | DEFAULT_COTROLLER_MODE, 97 | (F0THRESHOLD * 2), // connectTimeout 98 | DEFAUT_TEMPERATURE_HYSTERESIS, 99 | DEFAULT_EEPROM_QUOTA, 100 | DEFAULT_SEND_ALL, 101 | DEFAULT_COUNTER_PERIOD, 102 | DEFAULT_DATA_PACKETS_MODE, // sendDataPackets 103 | {} // packetStatus 104 | }; 105 | 106 | enum p1p2_Error : byte { 107 | P1P2_READ_OK, // Read OK 108 | P1P2_READ_ERROR, // Read Error 109 | P1P2_WRITE_OK, // Write OK 110 | P1P2_WRITE_ERROR, // Write Error 111 | P1P2_LAST // Number of status flags in this enum. Must be the last element within this enum!! 112 | }; 113 | 114 | enum udp_Error : byte { 115 | UDP_SENT, // Sent to UDP 116 | UDP_RECEIVED, // Received from UDP 117 | UDP_LAST // Number of status flags in this enum. Must be the last element within this enum!! 118 | }; 119 | 120 | typedef struct { 121 | uint32_t total; // Number of commands written to Daikin EEPROM 122 | byte date[6]; // Time and date when Daikin EEPROM write cycles counter started 123 | uint32_t dropped; // Number of commands dropped 124 | uint32_t invalid; // Number of commands invalid 125 | uint16_t today; // Number of commands written today 126 | uint16_t yesterday; // Number of commands written yesterday 127 | } eeprom_t; 128 | 129 | typedef struct { 130 | uint32_t eepromWrites; // Number of Arduino EEPROM write cycles 131 | eeprom_t eepromDaikin; 132 | byte major; // major version 133 | byte mac[6]; // MAC Address (initial value is random generated) 134 | config_t config; // configuration values 135 | byte statsDate[6]; // Time and date when stats counter started 136 | uint32_t p1p2Cnt[P1P2_LAST]; // array for storing P1P2 counters 137 | #ifdef ENABLE_EXTENDED_WEBUI 138 | uint32_t udpCnt[UDP_LAST]; // array for storing UDP counters 139 | #endif /* ENABLE_EXTENDED_WEBUI */ 140 | } data_t; 141 | 142 | data_t data; 143 | 144 | CircularBuffer cmdQueue; // queue of write commands 145 | 146 | 147 | /****** ETHERNET AND P1P2 SERIAL ******/ 148 | 149 | byte maxSockNum = MAX_SOCK_NUM; 150 | 151 | #ifdef ENABLE_DHCP 152 | bool dhcpSuccess = false; 153 | #endif /* ENABLE_DHCP */ 154 | 155 | EthernetUDP Udp; 156 | EthernetServer webServer(DEFAULT_CONFIG.webPort); 157 | 158 | #define SPI_CLK_PIN_VALUE (PINB & 0x20) 159 | 160 | P1P2Serial P1P2Serial; 161 | static byte hwID = 0; 162 | 163 | /****** TIMERS AND STATE MACHINE ******/ 164 | 165 | class Timer { 166 | private: 167 | uint32_t timestampLastHitMs; 168 | uint32_t sleepTimeMs; 169 | public: 170 | boolean isOver(); 171 | void sleep(uint32_t sleepTimeMs); 172 | }; 173 | boolean Timer::isOver() { 174 | if (uint32_t(millis() - timestampLastHitMs) > sleepTimeMs) { 175 | return true; 176 | } 177 | return false; 178 | } 179 | void Timer::sleep(uint32_t sleepTimeMs) { 180 | this->sleepTimeMs = sleepTimeMs; 181 | timestampLastHitMs = millis(); 182 | } 183 | 184 | Timer eepromTimer; // timer to delay writing statistics to EEPROM 185 | Timer connectionTimer; // timer to monitor connection status (connection to write to bus) 186 | Timer p1p2Timer; // timer to monitor P1P2 messages (reading from bus) 187 | Timer counterRequestTimer; // timer for 0xB8 counter requests 188 | Timer daikinNameTimer; // timer for requests for Daikin indoor and outdoor unit names (1 minute) 189 | byte counterRequest = 0; 190 | byte div2 = 0; 191 | 192 | enum state : byte { 193 | DISCONNECTED, 194 | CONNECTING, 195 | }; 196 | 197 | /*! 198 | @brief Status and peripheral address for the Arduino controller 199 | @return Status of the Arduino controller: 200 | - @c DISCONNECTED controller disconnected 201 | - @c CONNECTING controller connecting to first available address 202 | - @c Fx controller connected with address Fx 203 | */ 204 | byte controllerAddr = DISCONNECTED; 205 | 206 | /*! 207 | @brief Counts number of unanswered 00Fx30 requests, supports up to 16 addresses from F0 to FF 208 | @return Status of each address: 209 | - FxRequests[x] == 0 no 00Fx30 request was made (Fx address not supported by the pump) 210 | - FxRequests[x] < 0 other device is connected with Fx address 211 | - FxRequests[x] > 0 number of unasnwered requests for Fx address 212 | */ 213 | static int8_t FxRequests[16]; 214 | 215 | /****** RUN TIME AND DATA COUNTERS ******/ 216 | 217 | byte savedPackets[SAVED_PACKETS_SIZE] = {}; 218 | byte saved36Params[MAX_36_PARAMS + 1][2]; // storage for 0x36 parameters, param value is s16 (2 bytes) 219 | const byte PACKET_TYPE_HANDSHAKE = PACKET_TYPE_CONTROL[FIRST]; 220 | 221 | const byte NAME_SIZE = 16; // buffer size for device name 222 | char daikinIndoor[NAME_SIZE]; 223 | 224 | #ifdef ENABLE_EXTENDED_WEBUI 225 | char daikinOutdoor[NAME_SIZE]; 226 | #endif /* ENABLE_EXTENDED_WEBUI */ 227 | 228 | volatile uint32_t seed1; // seed1 is generated by CreateTrulyRandomSeed() 229 | volatile int8_t nrot; 230 | uint32_t seed2 = 17111989; // seed2 is static 231 | 232 | byte date[6]; // Date and time from Daikin Unit 233 | 234 | #ifdef ENABLE_EXTENDED_WEBUI 235 | // store uptime seconds (includes seconds counted before millis() overflow) 236 | uint32_t seconds; 237 | // store last millis() so that we can detect millis() overflow 238 | uint32_t last_milliseconds = 0; 239 | // store seconds passed until the moment of the overflow so that we can add them to "seconds" on the next call 240 | int32_t remaining_seconds; 241 | #endif /* ENABLE_EXTENDED_WEBUI */ 242 | 243 | /****** SETUP: RUNS ONCE ******/ 244 | 245 | void setup() { 246 | CreateTrulyRandomSeed(); 247 | EEPROM.get(DATA_START, data); 248 | // is configuration already stored in EEPROM? 249 | if (data.major != VERSION[0]) { 250 | data.major = VERSION[0]; 251 | // load default configuration from flash memory 252 | data.config = DEFAULT_CONFIG; 253 | // Send data packets (0x10-0x16) and counter packet (0xB8) by default 254 | setPacketStatus(PACKET_TYPE_COUNTER, PACKET_SENT, true); 255 | for (byte i = PACKET_TYPE_DATA[FIRST]; i <= PACKET_TYPE_DATA[LAST]; i++) { 256 | setPacketStatus(i, PACKET_SENT, true); 257 | } 258 | generateMac(); // generate new MAC (bytes 0, 1 and 2 are static, bytes 3, 4 and 5 are generated randomly) 259 | resetStats(); // resets all counters to 0 260 | updateEeprom(); 261 | } 262 | startEthernet(); 263 | 264 | memset(savedPackets, 0xFF, sizeof(savedPackets)); // initial value for all saved packets is 0xFF 265 | 266 | hwID = SPI_CLK_PIN_VALUE ? 0 : 1; 267 | P1P2Serial.begin(9600, hwID ? true : false, 6, 7); // if hwID = 1, use ADC6 and ADC7 268 | P1P2Serial.setEcho(true); // defines whether written data is read back and verified against written data (advise to keep this 1) 269 | P1P2Serial.setDelayTimeout(INIT_SDTO); 270 | 271 | connectionTimer.sleep(data.config.connectTimeout * 1000UL); 272 | eepromTimer.sleep(EEPROM_INTERVAL * 60UL * 60UL * 1000UL); // EEPROM_INTERVAL is in hours, sleep is in milliseconds! 273 | } 274 | 275 | void loop() { 276 | 277 | recvBus(); 278 | recvUdp(); 279 | manageSockets(); 280 | 281 | manageController(); 282 | 283 | if (rollover()) { 284 | resetStats(); 285 | updateEeprom(); 286 | } 287 | 288 | if (EEPROM_INTERVAL > 0 && eepromTimer.isOver() == true) { 289 | updateEeprom(); 290 | } 291 | 292 | #ifdef ENABLE_EXTENDED_WEBUI 293 | maintainUptime(); // maintain uptime in case of millis() overflow 294 | #endif /* ENABLE_EXTENDED_WEBUI */ 295 | #ifdef ENABLE_DHCP 296 | Ethernet.maintain(); 297 | #endif /* ENABLE_DHCP */ 298 | } 299 | -------------------------------------------------------------------------------- /arduino-altherma-controller/src/P1P2Serial_mod/P1P2Serial_ADC.h: -------------------------------------------------------------------------------- 1 | /* P1P2Serial_ADC.h: header file for ADC support in P1P2Serial 2 | * 3 | * Copyright (c) 2022 Arnold Niessen, arnold.niessen-at-gmail-dot-com - licensed under CC BY-NC-ND 4.0 with exceptions (see LICENSE.md) 4 | * 5 | * Version history 6 | * 20221029 v0.9.23 ADC code 7 | * 8 | */ 9 | 10 | // file included by P1P2Serial (P1P2Serial.h) and by P1P2-bridge-esp8266/P1P2MQTT in different locations, so keep header files in sync 11 | 12 | #define ADC_AVG_SHIFT 4 // sum 16 = 2^ADC_AVG_SHIFT samples before doing min/max check 13 | #define ADC_CNT_SHIFT 4 // sum 16384 = 2^(16-ADC_CNT_SHIFT) samples to V0avg/V1avg 14 | -------------------------------------------------------------------------------- /arduino-altherma-controller/src/P1P2Serial_mod/P1P2Serial_mod.h: -------------------------------------------------------------------------------- 1 | /* P1P2Serial: Library for reading/writing Daikin/Rotex P1P2 protocol 2 | * 3 | * Copyright (c) 2019-2022 Arnold Niessen, arnold.niessen-at-gmail-dot-com - licensed under CC BY-NC-ND 4.0 with exceptions (see LICENSE.md) 4 | * 5 | * Version history 6 | * 20221028 v0.9.23 ADC code 7 | * 20220918 v0.9.22 scopemode also for writes, focused on actual errors, fake error generation for test purposes, removing OLDP1P2LIB 8 | * 20220830 v0.9.18 version alignment with example programs 9 | * 20220817 v0.9.17 read-back-verification bug fixes in new and old library; config SERIALSPEED and OLDP1P2LIB selection depends on F_CPU 10 | * 20220811 v0.9.16 Added S_TIMER switch making TIMER0 use optional 11 | * 20220808 v0.9.15 LEDs on P1P2-ESP-Interface all on until first byte received on P1/P2 bus 12 | * 20220802 v0.9.14 major rewrite of send and receive method to spread CPU load over time and allow 8MHz ATmega operation 13 | * (until v0.9.18, old library version is still available as fall-back solution (#define OLDP1P2LIB below)) 14 | * 20220511 v0.9.12 various minor bug fixes 15 | * 20200109 v0.9.11 allow short pauses between bytes within a packet (for KLIC-DA device, to avoid detecting each byte as individual packet) 16 | * 20190914 v0.9.10 upon bus collision detection, write buffer is emptied 17 | * 20190914 v0.9.9 Added writeready() 18 | * 20190908 v0.9.8 Removed EOP signal in errorbuf results returned by readpacket(); as of now errorbuf contains only real error flags 19 | * 20190831 v0.9.7 Switch from TIMER0 to TIMER2 to avoid interference with millis() and readBytesUntil(), reduced RX_BUFFER_SIZE to 50 20 | * 20190824 v0.9.6 Added packetavailable() 21 | * 20190820 v0.9.5 Changed delay behaviour, timeout added 22 | * 20190817 v0.9.4 Clean up, bug fixes, improved ms counter, prescaler reset added, time measurement changed, delta/error reporting separated 23 | * 20190505 v0.9.3 Changed error handling and corrected deltabuf type in readpacket 24 | * 20190428 v0.9.2 Added setEcho(b), readpacket() and writepacket() 25 | * 20190409 v0.9.1 Improved setDelay() 26 | * 20190407 v0.9.0 Improved reading, writing, and meta-data; added support for timed writings and collision detection; added stand-alone hardware-debug mode 27 | * 20190303 v0.0.1 initial release; support for raw monitoring and hex monitoring 28 | * 29 | * Thanks to Krakra for providing the hints and references to the HBS and MM1192 documents on 30 | * https://community.openenergymonitor.org/t/hack-my-heat-pump-and-publish-data-onto-emoncms 31 | * to Bart Ellast for providing explanations and sample output from his heat pump, and 32 | * to Paul Stoffregen for publishing the AltSoftSerial library. 33 | * 34 | * The CC BY-NC-ND 4.0 licensed P1P2Serial library is based on the MIT-licensed AltSoftSerial, 35 | * but please note that P1P2Serial itself is licensed under the CC BY-NC-ND 4.0. 36 | * The original license for AltSoftSerial is included below. 37 | * 38 | ** An Alternative Software Serial Library ** 39 | ** http://www.pjrc.com/teensy/td_libs_AltSoftSerial.html ** 40 | ** Copyright (c) 2014 PJRC.COM, LLC, Paul Stoffregen, paul@pjrc.com ** 41 | ** ** 42 | ** Permission is hereby granted, free of charge, to any person obtaining a copy ** 43 | ** of this software and associated documentation files (the "Software"), to deal ** 44 | ** in the Software without restriction, including without limitation the rights ** 45 | ** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell ** 46 | ** copies of the Software, and to permit persons to whom the Software is ** 47 | ** furnished to do so, subject to the following conditions: ** 48 | ** ** 49 | ** The above copyright notice and this permission notice shall be included in ** 50 | ** all copies or substantial portions of the Software. ** 51 | ** ** 52 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ** 53 | ** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ** 54 | ** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ** 55 | ** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ** 56 | ** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ** 57 | ** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN ** 58 | ** THE SOFTWARE. ** 59 | * 60 | */ 61 | 62 | #ifndef P1P2Serial_h 63 | #define P1P2Serial_h 64 | 65 | #include 66 | #include "Arduino.h" 67 | #include "P1P2Serial_ADC.h" 68 | 69 | // Configuration options 70 | //#define MEASURE_LOAD // measures irq processing time 71 | // #define SW_SCOPE // records timing info of P1/P2 bus falling edges of start of the packets 72 | //#define GENERATE_FAKE_ERRORS // disable this for real use!! // only for NEWLIB, and on 8MHz this may add to the CPU load 73 | // #define SWS_FAKE_ERR_CNT 3000 // one fake error generated (per error type) per SWS_FAKE_ERR_CNT checks 74 | #define ALLOW_PAUSE_BETWEEN_BYTES 9 // If there is a pause between bytes on the bus which is longer than a 1/4 bit time, 75 | // P1P2Serial signals an end-of-packet. 76 | // Daikin devices do not add a pause between bytes, but some other controllers do, like the KLIC-DA from Zennios. 77 | // To avoid end-of-packet detection due to such an inter-byte pause, a pause between bytes of at most 78 | // ALLOW_PAUSE_BETWEEN_BYTES bit lengths is accepted; ALLOW_PAUSE_BETWEEN_BYTES should be less than 79 | // (65536 / Rticks_per_bit) - 2; for 16MHz at most ~37 (not sure if this value is still correct) 80 | // For KLICDA devices a value of 9 bit lengths seems to work. 81 | // #define S_TIMER // support for uptime_sec() in new library, but monopolizes TIMER0, so millis() cannot be used. 82 | // if undefined, TIMER0 is not used, and millis() can be used 83 | // if S_TIMER is undefined, the write budget (and error budget) will not increase over time TODO fix this 84 | // End of configuration options 85 | 86 | #define TX_BUFFER_SIZE 25 // write buffer size (1 more than max size needed) 87 | #define RX_BUFFER_SIZE 25 // read buffer (1 more than max size needed), should be <=254 88 | #define NO_HEAD2 0xFF 89 | 90 | 91 | #define ALTSS_BASE_FREQ F_CPU 92 | 93 | // Signalling of error conditions and end-of-packet: 94 | // (changed signalling of error/EOP messages in v0.9.4) 95 | // Use 16 bits for timing information 96 | // Use 8 bits for error code 97 | 98 | // read-back-verify errors 99 | #define ERROR_SB 0x01 // start bit error during write 100 | #define ERROR_BE 0x02 // data read-back error, likely bus collission 101 | #define ERROR_BC 0x20 // high bit half read-back error, likely bus collission 102 | 103 | // read errors 104 | #define ERROR_PE 0x04 // parity error 105 | 106 | // read + read-back-verify errors 107 | #define ERROR_OR 0x08 // read buffer overrun 108 | #define ERROR_CRC 0x10 // CRC error 109 | // 0x20, 0x40, available for other errors 110 | #define SIGNAL_EOP 0x80 // signaling end of packet, this is not an error flag 111 | 112 | #define ERROR_REAL_MASK 0x7F // Error mask to remove SIGNAL_EOP and fake errors 113 | #ifdef GENERATE_FAKE_ERRORS 114 | #define ERROR_FLAGS 0xFF7F 115 | #else /* GENERATE_FAKE_ERRORS */ 116 | #define ERROR_FLAGS 0x7F 117 | #endif /* GENERATE_FAKE_ERRORS */ 118 | 119 | #ifdef MEASURE_LOAD 120 | extern volatile uint16_t irq_w, irq_r, irq_lapsed_w, irq_lapsed_r; 121 | extern volatile uint8_t irq_busy; 122 | #endif 123 | 124 | //extern volatile uint8_t toolate; 125 | //extern volatile uint16_t lateness; 126 | 127 | #define SWS_MAX 22 // Max # SWS events recorded, limited by ATmega memory size (3 bytes/event) and serial output bandwidth 128 | // this cannot be much higher or time-info output overwhelms and crashes ESP8266, 21 is just over 1 byte timing info 129 | // info exchange not very clean but using global variables 130 | 131 | #define SWS_EVENT_LOOP 0xF0 // stored in sws_event[SWS_MAX - 1] to indicate that