From 1a9eb82d0f605b4377b5ca306c9d91eb3dd4535e Mon Sep 17 00:00:00 2001 From: Francesco Zanin Date: Tue, 30 Jun 2026 15:50:26 +0200 Subject: [PATCH] Initial commit: Hitachi RAR-6NE1 climate via ESP32/ESPHome Reverse engineering del protocollo IR (HITACHI_AC 28 byte) del telecomando RAR-6NE1 e custom component ESPHome bidirezionale (TX + RX) per Home Assistant. - esphome/: custom component hitachi_rar6ne1 (climate_ir::ClimateIR) + config - src/: firmware Arduino di cattura IR con web UI (strumento di diagnostica) - README.md: documentazione completa (protocollo, decode, checksum, gotcha) - Segreti esclusi dal versionamento (vedi *.example e .gitignore) - Licenza GPL-3.0 Co-Authored-By: Claude Opus 4.8 --- .gitignore | 15 + .vscode/extensions.json | 10 + LICENSE | 674 ++++++++++++++++++ README.md | 232 ++++++ .../components/hitachi_rar6ne1/__init__.py | 5 + esphome/components/hitachi_rar6ne1/climate.py | 20 + .../hitachi_rar6ne1/hitachi_rar6ne1.cpp | 256 +++++++ .../hitachi_rar6ne1/hitachi_rar6ne1.h | 73 ++ esphome/hitachi-ir.yaml | 96 +++ esphome/secrets.yaml.example | 12 + hitachi-ir-dump.yaml | 29 + platformio.ini | 7 + secrets.yaml.example | 5 + src/main.cpp | 364 ++++++++++ src/secrets.h.example | 15 + 15 files changed, 1813 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/extensions.json create mode 100644 LICENSE create mode 100644 README.md create mode 100644 esphome/components/hitachi_rar6ne1/__init__.py create mode 100644 esphome/components/hitachi_rar6ne1/climate.py create mode 100644 esphome/components/hitachi_rar6ne1/hitachi_rar6ne1.cpp create mode 100644 esphome/components/hitachi_rar6ne1/hitachi_rar6ne1.h create mode 100644 esphome/hitachi-ir.yaml create mode 100644 esphome/secrets.yaml.example create mode 100644 hitachi-ir-dump.yaml create mode 100644 platformio.ini create mode 100644 secrets.yaml.example create mode 100644 src/main.cpp create mode 100644 src/secrets.h.example diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a23a66 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# --- PlatformIO / build --- +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch + +# --- Segreti (NON committare) --- +# Credenziali reali: tenere fuori dal versionamento. Versionati solo i *.example. +src/secrets.h +secrets.yaml +esphome/secrets.yaml + +# --- Tooling locale --- +.claude/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d2d731a --- /dev/null +++ b/README.md @@ -0,0 +1,232 @@ +# Hitachi RAR-6NE1 — Reverse Engineering protocollo IR per ESPHome + +## Obiettivo +Decodificare il protocollo IR del telecomando Hitachi **RAR-6NE1** (compatibile con unità interne RAK-15QPE/18RPE/25RPE/35RPE/42RPE/50RPE) per realizzare un climate control via ESP32 + ESPHome. + +## Stato attuale + +> **2026-06-30 — Implementazione ESPHome realizzata e funzionante in bidirezionale.** +> Il condizionatore si comanda da Home Assistant (TX) e lo stato si sincronizza +> quando si usa il telecomando fisico (RX). Vedi la sezione **[Implementazione +> ESPHome (fase finale)](#implementazione-esphome-fase-finale)** in fondo per +> architettura, file e i "gotcha" risolti. + +Reverse engineering **sostanzialmente completo** sul lato protocollo. Mappatura solida di tutti i campi principali (power, mode, temp, fan, swing, silent, powerful) e **algoritmo di checksum determinato e verificato al 100%** — coincide esattamente con `IRHitachiAc::calcChecksum()` della libreria IRremoteESP8266: il telecomando RAR-6NE1 è quindi compatibile byte-per-byte e bit-per-bit con quella classe, non solo strutturalmente. Possiamo generare comandi custom validi da zero. + +L'ipotesi "frame periodico di temperatura ambiente dal telecomando" (sensore IR lato trasmettitore) è stata **verificata e scartata**: con la web UI di monitoraggio (vedi sotto) si è osservato un periodo di **~80 minuti di silenzio totale a telecomando fermo, con unità accesa**, e tutti i frame ricevuti sono risultati essere comandi da pressione tasto. Il telecomando **non invia trasmissioni spontanee periodiche**. + +L'implementazione finale ESPHome **non** ha riusato la classe `IRHitachiAc`: è un +custom component che estende `climate_ir::ClimateIR` e costruisce i 28 byte + +checksum a mano (più portabile, nessuna dipendenza da IRremoteESP8266). Dettagli +nella sezione *Implementazione ESPHome (fase finale)* in fondo. + +## File in questa cartella + +| File | Scopo | +|---|---| +| `platformio.ini` + `src/main.cpp` | Progetto PlatformIO. Lo sketch cattura i segnali IR (ricevitore su **GPIO27**, ESP32 DevKitC) e li **logga in una web UI** accessibile in WiFi, con timestamp NTP, decodifica a 28 byte on-device e verifica checksum. Pensato per lasciare l'ESP vicino al condizionatore e osservare i frame nel tempo (vedi sezione "Monitor web"). Logga anche su seriale (formato `IRrecvDumpV2`) se collegato via USB. | +| `src/secrets.h` | Credenziali WiFi + hostname mDNS + timezone per lo sketch PlatformIO (formato C, da compilare). **Distinto da `secrets.yaml`.** | +| `hitachi-ir-dump.yaml` | Config ESPHome alternativa per dump raw via `remote_receiver` (`dump: all`). **Nota**: ESPHome non ha un decoder Hitachi generico in `remote_base` (il decode Hitachi esiste solo dentro i climate component `hitachi_ac344`/`hitachi_ac424`), quindi questo file produce solo raw non interpretato — usare il progetto PlatformIO per il riconoscimento automatico del protocollo. | +| `secrets.yaml` | Credenziali WiFi per la config ESPHome (placeholder, da compilare). | + +## Monitor web (osservazione frame nel tempo) + +Lo sketch `src/main.cpp` espone una web UI per registrare ogni trama IR ricevuta — utile per lasciare l'ESP32 vicino al condizionatore e verificare, ad esempio, se il telecomando (che ha un sensore di temperatura integrato) invia frame periodici "spontanei" all'unità. + +**Setup:** +1. Compila `src/secrets.h` con SSID/password della rete WiFi (la stessa del PC da cui interroghi l'endpoint). +2. `pio run -t upload` con ESP collegato via USB, poi sposta l'ESP vicino al condizionatore (alimentato). +3. Apri `http://hitachi-ir.local/` (mDNS) o `http:///` (l'IP è stampato sul seriale al boot). + +**Endpoint:** +| Endpoint | Risposta | +|---|---| +| `GET /` | Pagina HTML con tabella auto-refresh (ogni 3s): orario, Δ dal frame precedente, bits, rawlen, checksum OK/BAD, campi decodificati (power/mode/temp/fan/swing/silent/powerful/tasto) e i 28 byte. | +| `GET /api/captures` | JSON riassuntivo di tutte le catture nel ring buffer (ultime 30) + uptime/totale/IP. Per polling automatico. | +| `GET /api/raw?i=N` | Array raw in microsecondi della cattura con sequenza `N` (per ri-analizzare frame anomali che non decodificano a 28 byte). | +| `GET /api/clear` | Svuota il buffer. | + +Il ring buffer tiene le ultime 30 catture in RAM (si perde al reset). Ogni cattura conserva anche il raw grezzo (fino a 460 valori) così i frame di lunghezza/struttura inattesa non vanno persi. + +## Perché non è un protocollo "già supportato" + +- Verificato su esphome.io: i soli climate component Hitachi nativi sono `hitachi_ac344` e `hitachi_ac424`. +- Verificato su `ir_Hitachi.h` (libreria IRremoteESP8266): i modelli di telecomando documentati per ciascuna variante (AC1, AC2, AC3, AC264, AC296, AC344, AC424) **non includono RAR-6NE1** né le sue varianti (RAR-6N1/6N2/6N3/6NE4/6N5). +- Empiricamente: lo sketch `IRrecvDumpV2`, che prova il decode con tutti i protocolli noti, riconosce solo *alcune* trame (es. lo stato OFF) come `HITACHI_AC` valido; la maggior parte risulta `UNKNOWN`. + +## Metodo di decode (validato) + +Le trame, una volta corrette per un bug di cattura ricorrente, risultano essere il protocollo **`HITACHI_AC` standard a 28 byte (224 bit)** — la classe più "base" della libreria (distinta da AC264/296/344/424) — con codifica a distanza: + +- Header: mark ~3300-3400µs, space ~1700µs +- Bit: mark costante ~400µs, space `526µs`→bit `0`, `1296µs`→bit `1` +- 28 byte totali, MSB-first per byte, ordine byte diretto (nessuna inversione) + +### Bug di cattura da correggere sempre +Molti dump (in particolare il primo dopo un periodo di idle) **perdono il mark iniziale lungo dell'header** (~3300-3400µs), registrando come primo valore solo la sua space (~1700µs). Risultato: l'array raw appare "shiftato" di una posizione. + +**Come riconoscerlo:** guarda il primo valore del raw array. +- Se è **> 3000µs** → header completo, **scarta i primi 2 valori** (mark+space header) prima di accoppiare il resto. +- Se è **< 2000µs** (tipicamente ~1650-1720) → header tagliato, **scarta solo il primo valore**. + +Poi accoppia i valori restanti `(mark, space)`: bit = `1` se `space > 800`, altrimenti `0`. Bit-pack MSB-first in byte, 28 byte totali. + +Questo metodo è stato calibrato e validato contro una trama riconosciuta nativamente come `HITACHI_AC` dalla libreria (stato OFF, con checksum verificato dalla libreria stessa) — non è una supposizione. + +## Mappatura campi (stato a 28 byte, indice da 0) + +| Campo | Byte | Valori osservati | Note | +|---|---|---|---| +| Power | `[17]` bit0 | `0x00`=OFF, `0x01`=ON | Confermato, coincide col layout documentato in `ir_Hitachi.h` | +| Mode | `[10]` | Cool=`0x20`, Fan-only=`0x30`, Auto=`0x40`, Dry=`0xA0`, Heat=`0xC0` | Confermato con test isolato (solo mode cambiato) | +| Temp | `[11]` | 26°C=`0x2C`, 25°C=`0x4C` | Azzerato a `0x01` in Fan-only (temp non applicabile) | +| Fan speed | `[13]` | `0x40`=1/4, `0xC0`=2/4, `0x20`=3/4, `0xA0`=4/4, `0x80`=Auto | Confermato con test isolato (sequenza 3→2→1→Auto→4→3). Dry forza automaticamente `0xC0`. | +| Checksum | `[27]` | Cambia ad ogni trasmissione | **Algoritmo determinato e verificato su tutti i 18 stati raccolti finora (match perfetto, nessuna eccezione)** — è esattamente `IRHitachiAc::calcChecksum()` della libreria IRremoteESP8266 (`ir_Hitachi.cpp:173-178`). Vedi sezione dedicata sotto. | +| Swing Verticale | `[14]` | `0xE0`=ON, `0x60`=OFF | Confermato con test isolato (sequenza V+H→solo H→OFF→solo V→V+H) | +| Swing Orizzontale | `[15]` | `0xE0`=ON, `0x60`=OFF | Confermato con lo stesso test | +| Silent | `[26]` bit4 (`0x10`) | `0x10`=ON, `0x00`=OFF | Confermato con test isolato ON→OFF | +| Powerful | `[25]` bit2 (`0x04`) | `0x34`=ON, `0x30`=OFF | Confermato con test isolato ON→OFF | +| `[9]` | — | `0xE0` (idle/solo mode), `0xC0` (tasto power on/off), `0xB0` (tasto swing V), `0x70` (tasto swing H), `0x30` (tasto fan speed), `0x38` (tasto Silent), `0xA8` (tasto Powerful) | **Non è uno stato persistente**: codifica il *tasto/categoria appena premuto* sul telecomando, non il risultato — resta identico tra ON e OFF dello stesso tasto (es. `0x38` sia per Silent ON che OFF, `0xC0` sia per power ON che OFF). Confermato su 15+ trasmissioni isolate (swing, fan, silent, powerful, power). | + +## Algoritmo di checksum (confermato) + +Coincide esattamente con la classe `IRHitachiAc` standard della libreria IRremoteESP8266 (`ir_Hitachi.cpp`, righe 173-178). Verificato in Python su tutti gli 18 stati raccolti (baseline mode/temp/power, swing, fan speed) — match perfetto al 100%, nessuna eccezione: + +```python +def reverse_bits(x, nbits=8): + r = 0 + for i in range(nbits): + r |= ((x >> i) & 1) << (nbits - 1 - i) + return r + +def calc_checksum(state): # state = 28 byte, l'ultimo e' il checksum stesso + s = 62 + for b in state[:-1]: # primi 27 byte + s = (s - reverse_bits(b, 8)) & 0xFF + return reverse_bits(s, 8) +``` + +Implicazione: possiamo generare comandi custom mai catturati (es. combinazioni nuove di mode/temp/fan/swing) calcolando il checksum con questa formula, senza dover fare solo replay di raw catturati. Probabile che la classe `IRHitachiAc` esistente nella libreria possa essere riusata direttamente (o con un wrapper minimale) per l'implementazione ESPHome finale, anziché scrivere un protocollo custom da zero — da verificare. + +### Stati di riferimento catturati (esadecimale, 28 byte) + +``` +OFF : 80 08 0C 02 FD 80 7F 88 48 C0 20 2C 00 20 E0 E0 00 00 00 00 00 00 00 01 80 30 00 40 +ON (26C cool, fan 3/4): 80 08 0C 02 FD 80 7F 88 48 C0 20 2C 00 20 E0 E0 00 01 00 00 00 00 00 01 80 30 00 41 +Cool 25°C : 80 08 0C 02 FD 80 7F 88 48 90 20 4C 00 20 E0 E0 00 01 00 00 00 00 00 01 80 30 00 7E +Fan only : 80 08 0C 02 FD 80 7F 88 48 E0 30 01 00 20 E0 E0 00 01 00 00 00 00 00 01 80 30 00 54 +Auto : 80 08 0C 02 FD 80 7F 88 48 E0 40 2C 00 20 E0 E0 00 01 00 00 00 00 00 01 80 30 00 01 +Heat : 80 08 0C 02 FD 80 7F 88 48 E0 C0 2C 00 20 E0 E0 00 01 00 00 00 00 00 01 80 30 00 FE +Dry : 80 08 0C 02 FD 80 7F 88 48 E0 A0 2C 00 C0 E0 E0 00 01 00 00 00 00 00 01 80 30 00 7E +Cool (return): 80 08 0C 02 FD 80 7F 88 48 E0 20 2C 00 20 E0 E0 00 01 00 00 00 00 00 01 80 30 00 7E +Fan 2/4 : 80 08 0C 02 FD 80 7F 88 48 30 20 2C 00 C0 E0 E0 00 01 00 00 00 00 00 01 80 30 00 5E +``` + +## Come riprodurre/continuare il lavoro + +1. **Hardware**: ESP32 DevKitC + ricevitore IR (TSOP38xx/VS1838B) su GPIO27, 3.3V. +2. **Cattura**: `pio run -t upload -t monitor` da questa cartella (richiede PlatformIO). Puntare il telecomando e premere un tasto. +3. **Decode manuale**: usare il metodo sopra descritto (vedi anche script Python di calibrazione usati in sessione, non salvati in questa cartella — ricostruibili facilmente dal metodo descritto). +4. **Verifica checksum**: aprire `ir_Hitachi.cpp` su github.com/crankyoldgit/IRremoteESP8266 (classe `IRHitachiAc`, funzione `calcChecksum()`) e confrontare con gli stati di riferimento sopra. + +## Prossimi passi + +1. ~~**Implementazione ESPHome finale**~~ ✅ **FATTA** (2026-06-30) — custom component + `climate_ir`, vedi sezione *Implementazione ESPHome (fase finale)*. Resta solo lo + sweep interattivo completo dei comandi in TX da confermare sul campo. +2. (Opzionale) Test temperatura impostata su più valori per confermare la formula `tempC = reverseBits(byte[11]) / 2` su tutto il range (16–32°C), finora derivata da 25/26°C. + +### Verificato e chiuso +- **Frame spontanei / sensore di temperatura del telecomando**: ipotesi **scartata**. Monitoraggio via web UI con ~80 min di silenzio totale a telecomando fermo (unità accesa); tutti i frame ricevuti sono comandi da pressione tasto. Il RAR-6NE1 non trasmette periodicamente. +- **byte[9]**: confermato = codice del tasto premuto (non stato persistente). + +## Riferimenti +- https://esphome.io/components/climate/climate_ir — protocolli IR climate supportati da ESPHome +- https://github.com/crankyoldgit/IRremoteESP8266 — `src/ir_Hitachi.h` / `ir_Hitachi.cpp` +- Nota progetto su Obsidian: `projects/hitachi-ir-esphome.md` + +--- + +## Implementazione ESPHome (fase finale) + +Realizzata il **2026-06-30**. Sostituisce il monitor Arduino (`src/main.cpp`, che +resta come strumento di cattura/diagnostica) con un'integrazione ESPHome nativa che +espone il condizionatore come entità `climate` in Home Assistant, **bidirezionale**. + +### Architettura +Custom **external component** che estende `climate_ir::ClimateIR`. Non riusa la +classe `IRHitachiAc` della libreria: costruisce i 28 byte e il checksum a mano +(stesso algoritmo verificato) ed emette/decodifica via i componenti nativi ESPHome +`remote_transmitter`/`remote_receiver` (RMT). Vantaggi: nessuna dipendenza esterna, +entità climate completa, TX **e** RX (sync col telecomando fisico). + +### File (`esphome/`) +``` +esphome/ +├── hitachi-ir.yaml # config: wifi/api/ota + TX GPIO25 + RX GPIO27 + climate +├── secrets.yaml # template wifi +└── components/hitachi_rar6ne1/ + ├── __init__.py · climate.py # registrazione platform (API ESPHome 2025+/2026.x) + ├── hitachi_rar6ne1.h # classe, costanti timing, mappa fan/swing/preset + └── hitachi_rar6ne1.cpp # build 28 byte + checksum + transmit_state + on_receive +``` +Importazione: `hitachi-ir.yaml` e la cartella `components/` nella stessa directory di +config ESPHome (es. `/config/`), poi build/flash dall'add-on ESPHome di HA (primo +flash USB, poi OTA). + +### Hardware +- **RX**: ricevitore IR (TSOP/VS1838B) su **GPIO27** (invertito, pull-up). +- **TX**: LED IR + driver a transistor su **GPIO25** — `GPIO25 →[1k]→ base NPN`; + `3V3 →[100R]→ LED IR(A→K) → collector`; `emitter → GND`. Portante 38 kHz. + +### Mappatura campi → 28 byte (in `build_state_`) +Identica alla tabella sopra: mode `[10]`, temp `[11]`=`reverseBits(°C×2)`, fan `[13]` +(Quiet/Low/Med/High→1/2/3/4 + Auto), swing V `[14]`/H `[15]` (`0xE0`/`0x60`), power +`[17]` bit0, preset **Eco→Silent** `[26]` bit4, **Boost→Powerful** `[25]` bit2, +checksum `[27]`. `byte[9]` (codice tasto) impostato a `0xE0` via la costante +`kButtonCode` — non critico (vedi gotcha). + +### Gotcha risolti (utili per progetti AC simili su ESPHome/ESP32) +1. **API `climate_ir` dipende dalla versione.** Su 2024.12.x serviva la costante + `CLIMATE_IR_WITH_RECEIVER_SCHEMA` + `register_climate_ir`; dalla 2025.x in poi + gli helper a funzione `climate_ir_with_receiver_schema(cls)` + `new_climate_ir`. + Il `climate.py` attuale usa la forma moderna (testato su **2026.6.3**). +2. **`external_components: path` deve puntare alla cartella *contenitore*** + (`components`), non alla cartella del componente (`components/hitachi_rar6ne1`): + altrimenti `climate.py` viene scambiato per il core `climate` → *import circolare* + (`partially initialized module 'esphome.components.climate'`). +3. **RX troncato.** Il frame Hitachi è 224 bit ≈ 226 simboli RMT, ma l'ESP32 + classico ne cattura di default ~192 → frame tagliato, decode fallito (nessun + `RX ok`). Fix: `rmt_symbols: 384` + `receive_symbols: 384` sul `remote_receiver` + (su questa versione di ESPHome; il vecchio `memory_blocks` non esiste più). +4. **`byte[9]` non è critico.** L'unità accetta il frame full-state a prescindere: + in TX un OFF con `byte[9]=0xC0` ha spento il condizionatore; in RX il telecomando + reale ha inviato `byte[9]=0x10` (= il default di `IRHitachiAc::stateReset`). +5. Tenere `dump: all` **spento** in esercizio: con frame lunghi blocca il loop + ~200 ms (`remote_receiver took a long time`). Riattivare solo per debug raw. + +### Stato di verifica (2026-06-30) +- ✅ **TX**: comando OFF dall'ESP → condizionatore spento (emettitore e accettazione + frame provati sul campo). +- ✅ **RX**: telecomando fisico → frame completo decodificato, checksum valido, + entità `climate` in HA aggiornata (mode/temp/fan/swing letti correttamente). +- ✅ Build/validazione su ESPHome 2026.6.3; fix RMT e path confermati dai log. +- ⏳ **Da confermare sul campo**: sweep completo dei comandi *dalla card HA* in TX + (tutti i modi, range temp, tutte le fan, swing V/H/Both, preset Eco/Boost). Il + protocollo è interamente validato, quindi atteso funzionante; manca solo la prova + esaustiva interattiva. + +### Edge case & robustezza (2026-06-30, sera) +- **Range temperatura** dell'entità ristretto a **18–30 °C** (`kMinTempC`/`kMaxTempC` + in `hitachi_rar6ne1.h`), scelta conservativa. ⚑ *Flag aperto*: il range realmente + supportato dal RAR-6NE1 è da verificare agli estremi (16/32) — se accettati si può + riallargare. +- **Preset ↔ ventola accoppiati** (override `control()` in `hitachi_rar6ne1.cpp`): + come il telecomando reale, attivando Eco/Silent la ventola va al minimo (Quiet) e + Boost/Powerful al massimo (High); cambiando la ventola si esce dal preset. Evita + stati incoerenti in HA (es. Boost + Quiet). +- **Setpoint in modalità Auto**: confermato **assoluto** come in Cool (nessuna + modifica necessaria). +- **OFF**: confermato funzionante in **ogni** modo (`byte[9]=0xE0`). +- ⏳ **In sospeso**: semantica della temperatura in **Dry** — `byte[11]` reale da + catturare dal telecomando (per ora si invia il setpoint come in Cool). diff --git a/esphome/components/hitachi_rar6ne1/__init__.py b/esphome/components/hitachi_rar6ne1/__init__.py new file mode 100644 index 0000000..7070252 --- /dev/null +++ b/esphome/components/hitachi_rar6ne1/__init__.py @@ -0,0 +1,5 @@ +# External component ESPHome per il telecomando Hitachi RAR-6NE1 +# (protocollo HITACHI_AC a 28 byte / 224 bit). +# +# La piattaforma vera e' definita in climate.py (platform: hitachi_rar6ne1). +CODEOWNERS = ["@francescozanin"] diff --git a/esphome/components/hitachi_rar6ne1/climate.py b/esphome/components/hitachi_rar6ne1/climate.py new file mode 100644 index 0000000..d7c4ede --- /dev/null +++ b/esphome/components/hitachi_rar6ne1/climate.py @@ -0,0 +1,20 @@ +import esphome.codegen as cg +from esphome.components import climate_ir + +# API ESPHome recente (>= 2025.x): helper a funzione +# climate_ir_with_receiver_schema(class) + new_climate_ir(config). +# NB: non disponibile su 2024.12.x (li' era CLIMATE_IR_WITH_RECEIVER_SCHEMA +# costante + register_climate_ir). Aggiornare ESPHome. +AUTO_LOAD = ["climate_ir"] +CODEOWNERS = ["@francescozanin"] + +hitachi_rar6ne1_ns = cg.esphome_ns.namespace("hitachi_rar6ne1") +HitachiRar6ne1Climate = hitachi_rar6ne1_ns.class_( + "HitachiRar6ne1Climate", climate_ir.ClimateIR +) + +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(HitachiRar6ne1Climate) + + +async def to_code(config): + await climate_ir.new_climate_ir(config) diff --git a/esphome/components/hitachi_rar6ne1/hitachi_rar6ne1.cpp b/esphome/components/hitachi_rar6ne1/hitachi_rar6ne1.cpp new file mode 100644 index 0000000..aa62ca4 --- /dev/null +++ b/esphome/components/hitachi_rar6ne1/hitachi_rar6ne1.cpp @@ -0,0 +1,256 @@ +#include "hitachi_rar6ne1.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include +#include + +namespace esphome { +namespace hitachi_rar6ne1 { + +static const char *const TAG = "hitachi_rar6ne1.climate"; + +// Stato di riferimento reale del RAR-6NE1 (ON 26C Cool). Fornisce tutti i byte +// "fissi" (intestazione/costanti); i campi variabili vengono sovrascritti in +// build_state_(). Indici: 0..27. +static const uint8_t BASE_STATE[kStateLength] = { + 0x80, 0x08, 0x0C, 0x02, 0xFD, 0x80, 0x7F, 0x88, + 0x48, 0xC0, 0x20, 0x2C, 0x00, 0x20, 0xE0, 0xE0, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x80, 0x30, 0x00, 0x41}; + +// Frame OFF realmente catturato dal RAR-6NE1 (checksum 0x40 verificato): usato +// dal test self-loopback per avere un riferimento deterministico e sicuro. +static const uint8_t REF_OFF[kStateLength] = { + 0x80, 0x08, 0x0C, 0x02, 0xFD, 0x80, 0x7F, 0x88, + 0x48, 0xC0, 0x20, 0x2C, 0x00, 0x20, 0xE0, 0xE0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x80, 0x30, 0x00, 0x40}; + +// Inverte l'ordine dei bit di un byte (MSB<->LSB). +static inline uint8_t reverse_bits8(uint8_t x) { + uint8_t r = 0; + for (uint8_t i = 0; i < 8; i++) + r |= static_cast(((x >> i) & 0x01) << (7 - i)); + return r; +} + +uint8_t HitachiRar6ne1Climate::calc_checksum_(const uint8_t state[kStateLength]) { + uint8_t sum = 62; + for (uint8_t i = 0; i < kStateLength - 1; i++) + sum -= reverse_bits8(state[i]); + return reverse_bits8(sum); +} + +void HitachiRar6ne1Climate::build_state_(uint8_t state[kStateLength]) { + std::memcpy(state, BASE_STATE, kStateLength); + + // byte[9]: codice tasto (vedi nota in .h). Variabile #1 da testare. + state[9] = kButtonCode; + + const bool power_on = this->mode != climate::CLIMATE_MODE_OFF; + + // Mode (byte[10]). In OFF teniamo Cool come modalita' di base nel frame. + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: state[10] = 0x20; break; + case climate::CLIMATE_MODE_HEAT: state[10] = 0xC0; break; + case climate::CLIMATE_MODE_DRY: state[10] = 0xA0; break; + case climate::CLIMATE_MODE_FAN_ONLY: state[10] = 0x30; break; + case climate::CLIMATE_MODE_HEAT_COOL: state[10] = 0x40; break; + case climate::CLIMATE_MODE_OFF: + default: state[10] = 0x20; break; + } + + // Temp (byte[11]) = reverseBits(C*2). In Fan-only non si applica -> 0x01. + if (this->mode == climate::CLIMATE_MODE_FAN_ONLY) { + state[11] = 0x01; + } else { + float tc = clamp(this->target_temperature, kMinTempC, kMaxTempC); + uint8_t t = static_cast(lroundf(tc)); + state[11] = reverse_bits8(static_cast(t * 2)); + } + + // Fan speed (byte[13]). In Dry l'unita' forza 2/4 (0xC0). + if (this->mode == climate::CLIMATE_MODE_DRY) { + state[13] = 0xC0; + } else { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) { + case climate::CLIMATE_FAN_QUIET: state[13] = 0x40; break; // 1/4 + case climate::CLIMATE_FAN_LOW: state[13] = 0xC0; break; // 2/4 + case climate::CLIMATE_FAN_MEDIUM: state[13] = 0x20; break; // 3/4 + case climate::CLIMATE_FAN_HIGH: state[13] = 0xA0; break; // 4/4 + case climate::CLIMATE_FAN_AUTO: + default: state[13] = 0x80; break; + } + } + + // Swing V (byte[14]) / H (byte[15]): 0xE0 = on, 0x60 = off. + const bool swing_v = this->swing_mode == climate::CLIMATE_SWING_VERTICAL || + this->swing_mode == climate::CLIMATE_SWING_BOTH; + const bool swing_h = this->swing_mode == climate::CLIMATE_SWING_HORIZONTAL || + this->swing_mode == climate::CLIMATE_SWING_BOTH; + state[14] = swing_v ? 0xE0 : 0x60; + state[15] = swing_h ? 0xE0 : 0x60; + + // Power (byte[17] bit0). + if (power_on) + state[17] |= 0x01; + else + state[17] &= ~0x01; + + // Preset: Eco -> Silent (byte[26] bit4), Boost -> Powerful (byte[25] bit2). + const climate::ClimatePreset preset = + this->preset.value_or(climate::CLIMATE_PRESET_NONE); + if (preset == climate::CLIMATE_PRESET_ECO) + state[26] |= 0x10; + else + state[26] &= ~0x10; + if (preset == climate::CLIMATE_PRESET_BOOST) + state[25] |= 0x04; + else + state[25] &= ~0x04; + + // Checksum (byte[27]). + state[27] = calc_checksum_(state); +} + +void HitachiRar6ne1Climate::transmit_frame_(const uint8_t state[kStateLength]) { + ESP_LOGD(TAG, + "TX: mode[10]=0x%02X temp[11]=0x%02X fan[13]=0x%02X " + "pwr[17]=0x%02X tasto[9]=0x%02X ck[27]=0x%02X", + state[10], state[11], state[13], state[17], state[9], state[27]); + + auto transmit = this->transmitter_->transmit(); + auto *data = transmit.get_data(); + data->set_carrier_frequency(kCarrierFrequency); + data->reserve(2 + kStateLength * 8 * 2 + 2); + + data->mark(kHeaderMark); + data->space(kHeaderSpace); + for (uint8_t i = 0; i < kStateLength; i++) { + for (int8_t bit = 7; bit >= 0; bit--) { // MSB-first + data->mark(kBitMark); + data->space((state[i] & (1 << bit)) ? kOneSpace : kZeroSpace); + } + } + data->mark(kBitMark); // footer mark + data->space(0); + + transmit.perform(); +} + +void HitachiRar6ne1Climate::transmit_state() { + uint8_t state[kStateLength]; + this->build_state_(state); + this->transmit_frame_(state); +} + +void HitachiRar6ne1Climate::send_test_frame() { + ESP_LOGI(TAG, "Test self-loopback: invio frame OFF di riferimento"); + this->transmit_frame_(REF_OFF); +} + +void HitachiRar6ne1Climate::control(const climate::ClimateCall &call) { + // Sul telecomando reale Silent e Powerful sono accoppiati alla ventola + // (Silent = minima, Powerful = massima) e mutuamente esclusivi. Teniamo lo + // stato coerente: attivando un preset forziamo la ventola corrispondente; + // se l'utente cambia la ventola mentre un preset e' attivo, usciamo dal preset. + climate::ClimateCall modified = call; + if (call.get_preset().has_value()) { + switch (*call.get_preset()) { + case climate::CLIMATE_PRESET_ECO: + modified.set_fan_mode(climate::CLIMATE_FAN_QUIET); + break; + case climate::CLIMATE_PRESET_BOOST: + modified.set_fan_mode(climate::CLIMATE_FAN_HIGH); + break; + default: + break; + } + } else if (call.get_fan_mode().has_value() && + this->preset.value_or(climate::CLIMATE_PRESET_NONE) != + climate::CLIMATE_PRESET_NONE) { + modified.set_preset(climate::CLIMATE_PRESET_NONE); + } + climate_ir::ClimateIR::control(modified); +} + +bool HitachiRar6ne1Climate::on_receive(remote_base::RemoteReceiveData data) { + if (!data.expect_item(kHeaderMark, kHeaderSpace)) + return false; + + uint8_t state[kStateLength] = {0}; + for (uint8_t i = 0; i < kStateLength; i++) { + for (int8_t bit = 7; bit >= 0; bit--) { // MSB-first + if (!data.expect_mark(kBitMark)) + return false; + if (data.expect_space(kOneSpace)) { + state[i] |= static_cast(1 << bit); + } else if (data.expect_space(kZeroSpace)) { + // bit a 0 + } else { + return false; + } + } + } + + if (state[27] != calc_checksum_(state)) { + ESP_LOGW(TAG, "RX: checksum errato, frame ignorato"); + return false; + } + + // Power / Mode (byte[17] bit0, byte[10]). + if ((state[17] & 0x01) == 0) { + this->mode = climate::CLIMATE_MODE_OFF; + } else { + switch (state[10]) { + case 0x20: this->mode = climate::CLIMATE_MODE_COOL; break; + case 0xC0: this->mode = climate::CLIMATE_MODE_HEAT; break; + case 0xA0: this->mode = climate::CLIMATE_MODE_DRY; break; + case 0x30: this->mode = climate::CLIMATE_MODE_FAN_ONLY; break; + case 0x40: this->mode = climate::CLIMATE_MODE_HEAT_COOL; break; + default: break; + } + } + + // Temp (byte[11]); non applicabile in Fan-only. + if (state[10] != 0x30) { + this->target_temperature = reverse_bits8(state[11]) / 2.0f; + } + + // Fan (byte[13]). + switch (state[13]) { + case 0x40: this->fan_mode = climate::CLIMATE_FAN_QUIET; break; + case 0xC0: this->fan_mode = climate::CLIMATE_FAN_LOW; break; + case 0x20: this->fan_mode = climate::CLIMATE_FAN_MEDIUM; break; + case 0xA0: this->fan_mode = climate::CLIMATE_FAN_HIGH; break; + case 0x80: this->fan_mode = climate::CLIMATE_FAN_AUTO; break; + default: break; + } + + // Swing (byte[14]/[15]). + const bool swing_v = state[14] == 0xE0; + const bool swing_h = state[15] == 0xE0; + if (swing_v && swing_h) + this->swing_mode = climate::CLIMATE_SWING_BOTH; + else if (swing_v) + this->swing_mode = climate::CLIMATE_SWING_VERTICAL; + else if (swing_h) + this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; + else + this->swing_mode = climate::CLIMATE_SWING_OFF; + + // Preset (byte[26] Silent / byte[25] Powerful). + if (state[26] & 0x10) + this->preset = climate::CLIMATE_PRESET_ECO; + else if (state[25] & 0x04) + this->preset = climate::CLIMATE_PRESET_BOOST; + else + this->preset = climate::CLIMATE_PRESET_NONE; + + ESP_LOGD(TAG, "RX ok (tasto byte[9]=0x%02X)", state[9]); + this->publish_state(); + return true; +} + +} // namespace hitachi_rar6ne1 +} // namespace esphome diff --git a/esphome/components/hitachi_rar6ne1/hitachi_rar6ne1.h b/esphome/components/hitachi_rar6ne1/hitachi_rar6ne1.h new file mode 100644 index 0000000..df4a618 --- /dev/null +++ b/esphome/components/hitachi_rar6ne1/hitachi_rar6ne1.h @@ -0,0 +1,73 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace hitachi_rar6ne1 { + +// Timing del protocollo HITACHI_AC (28 byte), verificati su IRremoteESP8266 +// (ir_Hitachi.cpp) e contro le catture reali del RAR-6NE1. +static const uint16_t kHeaderMark = 3300; +static const uint16_t kHeaderSpace = 1700; +static const uint16_t kBitMark = 400; +static const uint16_t kOneSpace = 1250; +static const uint16_t kZeroSpace = 500; +static const uint32_t kCarrierFrequency = 38000; + +static const uint8_t kStateLength = 28; + +// byte[9] = "codice tasto premuto" sul telecomando reale (0xE0 mode/idle, +// 0xC0 power, ...). L'unita' molto probabilmente lo ignora quando riceve uno +// stato completo, ma e' la PRIMA variabile da provare se i comandi sintetizzati +// non vengono accettati. Vedi README / nota Obsidian. +static const uint8_t kButtonCode = 0xE0; + +// Limiti dell'entita' climate. RANGE CONSERVATIVO 18-30 scelto per sicurezza. +// FLAG: DA VERIFICARE contro il range realmente supportato dal RAR-6NE1 (alcuni +// Hitachi arrivano a 16 in Cool e/o 32, altri si fermano prima). Vedi "Blocchi +// attivi" nella nota Obsidian projects/hitachi-ir-esphome. +static const float kMinTempC = 18.0f; +static const float kMaxTempC = 30.0f; +static const float kTempStep = 1.0f; + +class HitachiRar6ne1Climate : public climate_ir::ClimateIR { + public: + HitachiRar6ne1Climate() + : climate_ir::ClimateIR( + kMinTempC, kMaxTempC, kTempStep, + /*supports_dry=*/true, /*supports_fan_only=*/true, + // 4 velocita' + auto, mappate su modi standard HA: + // QUIET=1/4 LOW=2/4 MEDIUM=3/4 HIGH=4/4 AUTO + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_QUIET, + climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, + climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}, + // Eco = Silent, Boost = Powerful + {climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_ECO, + climate::CLIMATE_PRESET_BOOST}) {} + + // Invia un frame OFF realmente catturato (deterministico) per il test + // self-loopback: richiamabile da un button template nello YAML. + void send_test_frame(); + + // Accoppia preset e ventola come il telecomando fisico (Silent=ventola minima, + // Powerful=massima, mutuamente esclusivi) prima di applicare il comando. + void control(const climate::ClimateCall &call) override; + + protected: + // Costruisce il frame IR dallo stato climate corrente e lo trasmette. + void transmit_state() override; + // Decodifica un frame ricevuto (telecomando fisico) e aggiorna lo stato. + bool on_receive(remote_base::RemoteReceiveData data) override; + + // Riempie i 28 byte a partire dallo stato climate corrente (checksum incluso). + void build_state_(uint8_t state[kStateLength]); + // Emette i 28 byte sul trasmettitore IR (header + bit MSB-first + footer). + void transmit_frame_(const uint8_t state[kStateLength]); + // Checksum identico a IRHitachiAc::calcChecksum(). + static uint8_t calc_checksum_(const uint8_t state[kStateLength]); +}; + +} // namespace hitachi_rar6ne1 +} // namespace esphome diff --git a/esphome/hitachi-ir.yaml b/esphome/hitachi-ir.yaml new file mode 100644 index 0000000..923022c --- /dev/null +++ b/esphome/hitachi-ir.yaml @@ -0,0 +1,96 @@ +# Hitachi RAR-6NE1 -> climate ESPHome (protocollo HITACHI_AC, 28 byte). +# +# Da importare nell'add-on ESPHome di Home Assistant: +# - questo file e la cartella components/ vanno nella STESSA directory di +# config ESPHome (da te: /config/hitachi-ir.yaml + /config/components/) +# - quindi external_components -> path: components risolve a components/hitachi_rar6ne1/ +# - compila secrets.yaml (wifi) come da template accanto a questo file +# +# Hardware: +# - Ricevitore IR (TSOP/VS1838B) su GPIO27 (gia' presente) +# - Emettitore IR (LED IR + driver a transistor) su GPIO25 (DA MONTARE) +# GPIO25 --[1k]--> base NPN ; LED IR: 3V3 --[100R]--> A->K --> collector ; emitter -> GND + +esphome: + name: hitachi-ir + friendly_name: Condizionatore Hitachi + +esp32: + board: esp32dev + framework: + type: arduino + +# --- Connettivita' ----------------------------------------------------------- +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + # Hotspot di fallback se il WiFi non e' raggiungibile + ap: + ssid: "Hitachi-IR Fallback" + +captive_portal: + +logger: + level: DEBUG + +api: + encryption: + key: !secret api_encryption_key + +ota: + - platform: esphome + password: !secret ota_password + +# --- Component custom -------------------------------------------------------- +# path = cartella CHE CONTIENE la cartella del componente (components/), non il +# componente stesso. ESPHome cerchera' components/hitachi_rar6ne1/ = componente +# "hitachi_rar6ne1". Mettere components/hitachi_rar6ne1 qui causa un import +# circolare perche' climate.py verrebbe scambiato per il core 'climate'. +external_components: + - source: + type: local + path: components + +# --- IR: trasmettitore (invio comandi) --------------------------------------- +remote_transmitter: + id: tx + pin: GPIO25 + carrier_duty_percent: 50% + +# --- IR: ricevitore (sync col telecomando fisico) ---------------------------- +remote_receiver: + id: rx + pin: + number: GPIO27 + inverted: true + mode: + input: true + pullup: true + tolerance: 25% + # Il frame Hitachi e' 224 bit (~226 coppie = simboli RMT). Sull'ESP32 classico + # il default e' ~192 simboli -> il frame veniva TRONCATO e il decoder falliva. + # 1 simbolo RMT = 1 coppia mark/space. 384 da margine ampio e lascia memoria + # RMT al trasmettitore (totale 512 simboli su ESP32 classico). + rmt_symbols: 384 + receive_symbols: 384 + # DEBUG: riattiva solo per ispezionare i raw. Intasa i log e blocca il loop + # ~200ms per frame (stampa pronto) -> RX gia' validato, tenuto spento. + # dump: all + +# --- Entita' climate --------------------------------------------------------- +climate: + - platform: hitachi_rar6ne1 + id: hitachi_ac + name: "Condizionatore" + transmitter_id: tx + receiver_id: rx + +# --- DEBUG: pulsante per test self-loopback ---------------------------------- +# Premendolo, l'ESP trasmette un frame OFF di riferimento; se il LED IR illumina +# (anche di riflesso) il ricevitore, nei log compare "RX ok" + il dump dei byte. +# Rimuovi questa sezione a validazione conclusa. +button: + - platform: template + name: "Test IR loopback" + on_press: + - lambda: 'id(hitachi_ac).send_test_frame();' diff --git a/esphome/secrets.yaml.example b/esphome/secrets.yaml.example new file mode 100644 index 0000000..83a89f2 --- /dev/null +++ b/esphome/secrets.yaml.example @@ -0,0 +1,12 @@ +# Template segreti ESPHome. Copia in `esphome/secrets.yaml` (ignorato da git) e +# compila con i valori reali. Nell'add-on di Home Assistant il file vive in +# /config/esphome/secrets.yaml ed e' condiviso da tutte le config ESPHome: +# aggiungi LA' le stesse chiavi. + +wifi_ssid: "TUA_SSID" +wifi_password: "TUA_PASSWORD" + +# Chiave di cifratura API (32 byte base64) e password OTA per hitachi-ir.yaml. +# Genera la api key da ESPHome (dashboard) o con un generatore base64 a 32 byte. +api_encryption_key: "GENERA_UNA_CHIAVE_BASE64_32_BYTE=" +ota_password: "GENERA_UNA_PASSWORD_OTA" diff --git a/hitachi-ir-dump.yaml b/hitachi-ir-dump.yaml new file mode 100644 index 0000000..c78bf78 --- /dev/null +++ b/hitachi-ir-dump.yaml @@ -0,0 +1,29 @@ +esphome: + name: hitachi-ir-dump + +esp32: + board: esp32dev + framework: + type: arduino + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + +logger: + level: DEBUG + +api: +ota: + +remote_receiver: + pin: + number: GPIO27 + inverted: true + mode: + input: true + pullup: true + dump: all + tolerance: 55% + filter: 250us + idle: 30ms diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..7431b24 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,7 @@ +[env:esp32dev] +platform = espressif32 +board = esp32dev +framework = arduino +monitor_speed = 115200 +lib_deps = + crankyoldgit/IRremoteESP8266@^2.8.6 diff --git a/secrets.yaml.example b/secrets.yaml.example new file mode 100644 index 0000000..9261ea4 --- /dev/null +++ b/secrets.yaml.example @@ -0,0 +1,5 @@ +# Template segreti per la config ESPHome di cattura (hitachi-ir-dump.yaml). +# Copia in `secrets.yaml` e compila con i valori reali. `secrets.yaml` e' ignorato da git. + +wifi_ssid: "TUA_SSID" +wifi_password: "TUA_PASSWORD" diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..898650a --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,364 @@ +// Hitachi RAR-6NE1 - cattura IR con web UI per osservazione nel tempo. +// +// L'ESP32 (ricevitore IR su GPIO27) sta vicino al condizionatore e si collega +// in WiFi. Logga OGNI trama IR ricevuta (anche eventuali frame "spontanei" non +// generati da pressione tasti, es. ipotetico frame periodico del sensore di +// temperatura del telecomando) in un ring buffer in RAM, con timestamp. +// +// Endpoint: +// GET / -> pagina HTML con tabella auto-refresh +// GET /api/captures -> JSON riassuntivo di tutte le catture (per polling) +// GET /api/raw?i=N -> array raw (microsecondi) della cattura con seq N +// GET /api/clear -> svuota il buffer +// +// Accesso: http://hitachi-ir.local/ (mDNS) oppure http:/// + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "secrets.h" + +// --------------------------------------------------------------------------- +// Configurazione IR +// --------------------------------------------------------------------------- +const uint16_t kRecvPin = 27; +const uint16_t kCaptureBufferSize = 1024; +const uint8_t kTimeout = 50; +const uint16_t kMinUnknownSize = 12; + +IRrecv irrecv(kRecvPin, kCaptureBufferSize, kTimeout, true); +decode_results results; + +// --------------------------------------------------------------------------- +// Ring buffer delle catture +// --------------------------------------------------------------------------- +const uint16_t kRingSize = 30; // numero di catture mantenute +const uint16_t kMaxRawStore = 460; // valori raw memorizzati per cattura + +struct Capture { + uint32_t seq; // numero progressivo (dal boot) + uint32_t ms; // millis() all'istante della cattura + time_t epoch; // tempo NTP (0 se non sincronizzato) + uint16_t bits; // results.bits + uint16_t rawlen; // results.rawlen reale (puo' superare kMaxRawStore) + bool decoded; // true se siamo riusciti a estrarre 28 byte + bool checksumOk; // true se il checksum dei 28 byte e' valido + uint8_t state[28]; // stato decodificato (valido se decoded) + uint16_t rawCount; // valori raw effettivamente memorizzati + uint16_t raw[kMaxRawStore]; // tempi in microsecondi (mark/space alternati) +}; + +Capture ring[kRingSize]; +volatile uint32_t totalCaptures = 0; // totale ricevute dal boot +uint16_t ringHead = 0; // prossima posizione di scrittura + +WebServer server(80); + +// --------------------------------------------------------------------------- +// Decodifica a 28 byte (stesso metodo Python validato in sessione) +// --------------------------------------------------------------------------- +// Correzione del bug di cattura ricorrente sull'header: +// primo valore > 3000us -> header completo (mark+space), salta 2 +// primo valore ~1500-2000us -> header tagliato (solo space), salta 1 +// altrimenti -> nessun header, parti dal primo valore +// Poi accoppia (mark, space): bit = 1 se space > 800us. Pack MSB-first. +static bool decode28(const decode_results *r, uint8_t out[28]) { + for (uint8_t b = 0; b < 28; b++) out[b] = 0; + uint16_t n = r->rawlen; + if (n < 4) return false; + + // rawbuf[0] e' il gap iniziale; i dati partono da rawbuf[1]. + uint32_t first = (uint32_t)r->rawbuf[1] * kRawTick; + uint16_t idx; // indice del primo "mark" da accoppiare + if (first > 3000) + idx = 3; // salta mark + space dell'header + else if (first > 1500 && first < 2000) + idx = 2; // header tagliato: salta solo la space residua + else + idx = 1; // nessun header riconoscibile + + uint16_t bitcount = 0; + while ((uint16_t)(idx + 1) < n && bitcount < 224) { + uint32_t space = (uint32_t)r->rawbuf[idx + 1] * kRawTick; + uint8_t bit = (space > 800) ? 1 : 0; + out[bitcount / 8] = (uint8_t)((out[bitcount / 8] << 1) | bit); + bitcount++; + idx += 2; + } + return bitcount >= 224; +} + +// Checksum IRHitachiAc (ir_Hitachi.cpp): sum=62; sum-=reverseBits(byte) per i +// primi 27 byte; checksum = reverseBits(sum). +static uint8_t calcChecksum28(const uint8_t *st) { + uint8_t sum = 62; + for (uint8_t i = 0; i < 27; i++) sum -= (uint8_t)reverseBits(st[i], 8); + return (uint8_t)reverseBits(sum, 8); +} + +// --------------------------------------------------------------------------- +// Registrazione di una cattura nel ring buffer +// --------------------------------------------------------------------------- +static void storeCapture(const decode_results *r) { + Capture &c = ring[ringHead]; + c.seq = ++totalCaptures; + c.ms = millis(); + time_t now = time(nullptr); + c.epoch = (now > 1000000000) ? now : 0; // valido solo se NTP ha sincronizzato + c.bits = r->bits; + c.rawlen = r->rawlen; + + c.decoded = decode28(r, c.state); + c.checksumOk = c.decoded && (c.state[27] == calcChecksum28(c.state)); + + // Copia il raw grezzo (in microsecondi), saltando il gap iniziale [0]. + c.rawCount = 0; + for (uint16_t i = 1; i < r->rawlen && c.rawCount < kMaxRawStore; i++) { + c.raw[c.rawCount++] = (uint16_t)((uint32_t)r->rawbuf[i] * kRawTick); + } + + ringHead = (ringHead + 1) % kRingSize; +} + +// --------------------------------------------------------------------------- +// Pagina HTML +// --------------------------------------------------------------------------- +static const char PAGE_HTML[] PROGMEM = R"HTML( + + +Hitachi IR monitor + +

Hitachi RAR-6NE1 - IR monitor

+
+ Uptime: - + Totale frame: - + Ultimo frame: - fa + Host: - + + + auto-refresh 3s +
+ + + + + + +
#orarioboot(s)Δprec(s)bitsrawlenckpwrmodetempfanswVswHsilpwftasto28 byte
+)HTML"; + +// --------------------------------------------------------------------------- +// Handler HTTP +// --------------------------------------------------------------------------- +static String hexState(const uint8_t *st) { + String s; + char buf[4]; + for (uint8_t i = 0; i < 28; i++) { + snprintf(buf, sizeof(buf), "%02X", st[i]); + s += buf; + if (i < 27) s += ' '; + } + return s; +} + +static String isoTime(time_t epoch) { + if (epoch == 0) return String(""); + struct tm tmv; + localtime_r(&epoch, &tmv); + char buf[24]; + strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tmv); + return String(buf); +} + +static void handleRoot() { + server.send_P(200, "text/html", PAGE_HTML); +} + +static void handleCaptures() { + uint32_t nowMs = millis(); + String j = "{"; + j += "\"uptime\":" + String(nowMs / 1000); + j += ",\"total\":" + String(totalCaptures); + j += ",\"host\":\"" + String(MDNS_HOSTNAME) + ".local\""; + j += ",\"ip\":\"" + WiFi.localIP().toString() + "\""; + j += ",\"items\":["; + + // Itera dal piu' vecchio al piu' recente presente nel ring. + uint16_t count = (totalCaptures < kRingSize) ? totalCaptures : kRingSize; + uint16_t start = (totalCaptures < kRingSize) ? 0 + : ringHead; // il piu' vecchio + bool firstItem = true; + for (uint16_t k = 0; k < count; k++) { + uint16_t i = (start + k) % kRingSize; + Capture &c = ring[i]; + if (!firstItem) j += ','; + firstItem = false; + j += "{\"n\":" + String(c.seq); + j += ",\"ms\":" + String(c.ms); + j += ",\"ago\":" + String((nowMs - c.ms) / 1000); + j += ",\"t\":\"" + isoTime(c.epoch) + "\""; + j += ",\"bits\":" + String(c.bits); + j += ",\"rawlen\":" + String(c.rawlen); + j += ",\"decoded\":" + String(c.decoded ? 1 : 0); + j += ",\"ck\":" + String(c.checksumOk ? 1 : 0); + j += ",\"hex\":\"" + (c.decoded ? hexState(c.state) : String("")) + "\""; + j += "}"; + } + j += "]}"; + server.send(200, "application/json", j); +} + +static void handleRaw() { + if (!server.hasArg("i")) { + server.send(400, "text/plain", "manca parametro i"); + return; + } + uint32_t want = server.arg("i").toInt(); + uint16_t count = (totalCaptures < kRingSize) ? totalCaptures : kRingSize; + uint16_t start = (totalCaptures < kRingSize) ? 0 : ringHead; + for (uint16_t k = 0; k < count; k++) { + uint16_t i = (start + k) % kRingSize; + Capture &c = ring[i]; + if (c.seq == want) { + String j = "{\"n\":" + String(c.seq) + + ",\"rawlen\":" + String(c.rawlen) + + ",\"stored\":" + String(c.rawCount) + ",\"raw\":["; + for (uint16_t x = 0; x < c.rawCount; x++) { + if (x) j += ','; + j += String(c.raw[x]); + } + j += "]}"; + server.send(200, "application/json", j); + return; + } + } + server.send(404, "text/plain", "cattura non trovata (forse gia' uscita dal buffer)"); +} + +static void handleClear() { + totalCaptures = 0; + ringHead = 0; + server.send(200, "application/json", "{\"ok\":true}"); +} + +// --------------------------------------------------------------------------- +// Setup / loop +// --------------------------------------------------------------------------- +void setup() { + Serial.begin(115200); + delay(200); + Serial.println("\nHitachi IR monitor - avvio"); + + WiFi.mode(WIFI_STA); + WiFi.setHostname(MDNS_HOSTNAME); + WiFi.begin(WIFI_SSID, WIFI_PASSWORD); + Serial.print("Connessione WiFi"); + uint32_t t0 = millis(); + while (WiFi.status() != WL_CONNECTED && millis() - t0 < 20000) { + delay(300); + Serial.print('.'); + } + Serial.println(); + if (WiFi.status() == WL_CONNECTED) { + Serial.print("Connesso. IP: "); + Serial.println(WiFi.localIP()); + } else { + Serial.println("WiFi NON connesso - controlla secrets.h"); + } + + // NTP (non bloccante: si sincronizza in background) + configTzTime(TZ_INFO, "pool.ntp.org", "time.google.com"); + + // mDNS + if (MDNS.begin(MDNS_HOSTNAME)) { + MDNS.addService("http", "tcp", 80); + Serial.printf("mDNS attivo: http://%s.local/\n", MDNS_HOSTNAME); + } + + // Web server + server.on("/", handleRoot); + server.on("/api/captures", handleCaptures); + server.on("/api/raw", handleRaw); + server.on("/api/clear", handleClear); + server.begin(); + Serial.println("Web server avviato sulla porta 80"); + + // IR + irrecv.setUnknownThreshold(kMinUnknownSize); + irrecv.enableIRIn(); + Serial.println("Ricezione IR attiva su GPIO27"); +} + +void loop() { + server.handleClient(); + + if (irrecv.decode(&results)) { + storeCapture(&results); + // Log seriale (utile se mai collegato via USB) + Serial.println(resultToHumanReadableBasic(&results)); + Serial.println(resultToSourceCode(&results)); + Serial.println(); + } +} diff --git a/src/secrets.h.example b/src/secrets.h.example new file mode 100644 index 0000000..98a2524 --- /dev/null +++ b/src/secrets.h.example @@ -0,0 +1,15 @@ +#pragma once + +// Credenziali WiFi per lo sketch di cattura IR con web UI. +// Copia questo file in `src/secrets.h` e compila i valori con la tua rete +// (la stessa a cui e' collegato il PC da cui interroghi l'endpoint HTTP), +// poi ricompila/flasha. `src/secrets.h` e' ignorato da git. + +#define WIFI_SSID "TUA_SSID" +#define WIFI_PASSWORD "TUA_PASSWORD" + +// Hostname mDNS: raggiungerai la UI su http://hitachi-ir.local/ +#define MDNS_HOSTNAME "hitachi-ir" + +// Timezone (Italia, con ora legale). Formato POSIX TZ. +#define TZ_INFO "CET-1CEST,M3.5.0,M10.5.0/3"