├── .gitignore ├── Dockerfile ├── LICENSE ├── PLUGINS.md ├── README.md ├── REVERSE-ENGNEER.md ├── datalogger.py ├── docker-compose.yaml ├── duallog.py ├── img ├── Battery-Screenshot.png └── SRNE-Screenshot.png ├── plugins ├── Hacien │ ├── __init__.py │ └── dev │ │ ├── bms-2024-03-20-01.pcapng │ │ ├── bms-2024-03-20.07.pcapng │ │ ├── bms-2024-03-20.08.pcapng │ │ ├── bms-raw-2024-03-20.json │ │ ├── bms-session-2024-03-20-01.txt │ │ ├── bms-session.txt │ │ ├── bms.pcapng │ │ ├── parse.py │ │ └── read.py ├── Meritsun │ └── __init__.py ├── RenogyBatt │ └── __init__.py ├── SolarLink │ └── __init__.py ├── Topband │ └── __init__.py └── VEDirect │ └── __init__.py ├── requirements.txt ├── solar-monitor.ini.dist ├── solar-monitor.py ├── solar-monitor.service └── solardevice.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/launch.json 2 | solar-monitor.ini 3 | solar-monitor.log* 4 | __pycache__/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim-bullseye 2 | 3 | RUN apt update && apt -y install --no-install-recommends bluetooth build-essential cmake autoconf automake pkg-config libdbus-1-dev libglib2.0-dev libgirepository1.0-dev gcc libcairo2-dev pkg-config python3-dev gir1.2-gtk-3.0 4 | 5 | WORKDIR /solar-monitor 6 | COPY . . 7 | 8 | RUN pip install -r requirements.txt 9 | 10 | ENTRYPOINT [ "python", "-u", "solar-monitor.py" ] 11 | 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /PLUGINS.md: -------------------------------------------------------------------------------- 1 | # Write your own plugin 2 | 3 | Plugins are simple python modules, and only need two classes that are used by the main process. 4 | 5 | `class Config()` and `class Util()` 6 | 7 | They can be put in their own sub directory under "plugins/" and only require a `__init__.py` file so they can be imported by the solardevice.py script. 8 | 9 | See the existing plugins for examples. 10 | 11 | Reading and writing to a BT device is done using Service UUIDs and Characteristic UUIDs, where a Service UUID is the "parent" of one or more Characteristics. 12 | 13 | Typically, you can have a hierarchy like 14 | 15 | - Service UUID: 0000ffe**0**-0000-1000-8000-00805f9b34fb 16 | - Characteristics: 0000ffe**1**-0000-1000-8000-00805f9b34fb 17 | - Characteristics: 0000ffe**2**-0000-1000-8000-00805f9b34fb 18 | - Characteristics: 0000ffe**3**-0000-1000-8000-00805f9b34fb 19 | - Characteristics: 0000ffe**4**-0000-1000-8000-00805f9b34fb 20 | 21 | We then *subscribe to* or *write to* one or more of these characteristics. 22 | 23 | These UUIDs can be found either by reverse engineering the original app, by sniffing the packets going over the bluetooth connection from the original app, or by using various tools to connect directly to the device with bluetooth and poke around until you find something of interest. But note that many devices need some kind of "init" before they send any data. The easiest is probably to run e.g. wireshark to sniff the packets going over the air, and try to figure out what happens. 24 | 25 | ## Config 26 | The Config class defines some parameters for the plugin: 27 | 28 | - `DEVICE_ID` - Some devices send this as part of the notifications 29 | - `SEND_ACK` - Some devices require that an "ack" is returned for all received packets. If this parameter is set to `True` the `Util` class needs a function `ackData` that generates the ack packets 30 | - `NEED_POLLING` - Some devices require active polling, while others will just send a continous stream of updates. If this is set to `True`, the device will be polled every second for updates 31 | - `NOTIFY_SERVICE_UUID` - The UUID that contains the notifications 32 | - `NOTIFY_CHAR_UUID` - The characteristics within the `NOTIFY_SERVICE_UUID` that we will subscribe to. Can be a single UUID or a list of UUIDs 33 | - `WRITE_SERVICE_UUID`- The service UUID containing the characteristics we will send write requests to 34 | - `WRITE_CHAR_UUID_POLLING` - The charactersitcs UUID we send polling requests, acks etc. to 35 | - `WRITE_CHAR_UUID_COMMANDS` - The characteristics UUID we send data to for commands. E.g turing power on or off on a regulator etc. 36 | 37 | 38 | 39 | ## Util 40 | 41 | The Util class is bound to a PowerDevice object and is used to read, write and parse data from the physical devices. There is only a few functions that need to be exposed to the PowerDevice object: 42 | 43 | ### init 44 | __init__() of the class expects a `PowerDevice` (an object defined in `solardevice.py`) as its only parameter. The plugin will then update this device-object as data is recieved. 45 | 46 | ### Updates 47 | When we recieve an update, the class function `notificationUpdate(data, char)` is called with the raw data and the UUID of the characteristic we recieved the data from. This function is then responsible for parsing the data and will then update the `PowerDevice` object. The function should return True if the message was understood and handled, and False if it was not. 48 | 49 | ### Ack 50 | The class function `ackData(data)` is required if the device expects an ack for each notification it sends. This function must generate and return a valid "ack-packet" for the received `data` 51 | 52 | ### Polling 53 | If polling is required, the class function `pollRequest()` must return the packet we need to send to the device to poll if for new data. 54 | 55 | ### Commands 56 | Some devices accept commands, such as turning power on and off on an inverter. To send commands to a device, we call the function `cmdRequest(command, value)` with two paramters, the *command*, and a *value*. E.g. *command* = `power_switch` and *value* = `1 or `0` for "on" or "off". 57 | 58 | The function must return a *list* of packets that should be sent to the device. 59 | 60 | 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # solar-monitor 2 | 3 | This utility monitors defined BLE-devices, and sends parsed data to a remote server using either MQTT or json/HTTP 4 | 5 | Currently supported 6 | - SRNE regulators (monitored by the SolarLink APP: https://play.google.com/store/apps/details?id=com.shuorigf 7 | - Lithium Batteries: 8 | - monitored by the Meritsun APP: https://play.google.com/store/apps/details?id=com.meritsun.smartpower 9 | - monitored by Renogy DC Home APP: https://play.google.com/store/apps/details?id=com.renogy.dchome) 10 | - monitored by TBEnergy APP: https://play.google.com/store/apps/details?id=com.topband.smartpower 11 | - Victron Energy - VE.Direct devices - currently only Phoenix inverters are tested. Work in progress to add more devices 12 | - Renogy BT-1 (uses the same protocol as the SolarLink/SRNE) 13 | 14 | # Update 2025-01-31 15 | The latest updates adds threading to the application, so it will now poll each device in its own thread, and log data in a different thread. 16 | 17 | This ensures that issues with one connected device should no longer block and stop the other devices from logging. I am running ths version myself, and it seems to work fine, but any threading application is a potential risk, especially when it comes to resource usage, so please watch carefully after upgrading. 18 | 19 | # Requirements 20 | Look at requirements.txt 21 | 22 | Be aware that libscrc is NOT pip-installable on all versions of RPI, so you need to build it from source: https://github.com/hex-in/libscrc 23 | 24 | The monitor runs fine on a Raspberry Pi zero, making it ideal for monitoring places where there is no grid power, as it uses a minimal amount of power. 25 | 26 | # Docker 27 | 28 | To run the service as a container, you can use the included `docker-compose.yaml` 29 | 30 | * Copy `solar-monitor.ini.dist` to e.g `~/solar-monitor/solar-monitor.ini` 31 | * Edit the ini-file as per the instructions below. 32 | * Ensure that docker-compose.yaml has the right path to the ini-file 33 | * Run: 34 | 35 | ``` 36 | docker-compose up -d 37 | ``` 38 | in the same dir as you downloaded these files. This might take some time, depending on your build host. 39 | 40 | Check the logs with 41 | 42 | ``` 43 | docker logs solar-monitor 44 | ``` 45 | 46 | `network=host` is needed because access to bluetooth devices requires host network. 47 | 48 | 49 | # Running as a service 50 | 51 | You need the following: 52 | 53 | * solar-monitor.py The actual daemon 54 | * solardevice.py Extension of ble gatt and some classes to store the values that are read from the BLE-devices 55 | * duallog.py CLI and file-logger with multiple destinations 56 | * datalogger.py Class for pushing data to remote servers 57 | * plugins/* Implemetation of vendor specific BLE parsing 58 | 59 | Also 60 | 61 | * solar-monitor.service - A systemd service-description for auto-starting the service 62 | * solar-monitor.ini.dist Configuration-file. To be modified and renamed to solar-monitor.ini 63 | 64 | Copy solar-monitor.ini.dist to solar-monitor.ini, and add the correct mac addresses to your BLE devices (NOT your mobile phone with the app, but the actual battery/bluetooth adapter) 65 | 66 | Copy solar-monitor.service to /etc/systemd/system/ and run 67 | ``` 68 | systemctl daemon-reload 69 | systemctl enable solar-monitor 70 | systemctl start solar-monitor 71 | ``` 72 | The systemd unit file might need some adjustments to point to the right scripts 73 | 74 | Alternatively just run `solar-monitor.py` in something like termux or screen (might require root privileges to access bluetooth directly) 75 | 76 | 77 | # Output 78 | ``` 79 | 2020-06-22 13:34:09,149 INFO : Adapter status - Powered: True 80 | 2020-06-22 13:34:09,284 INFO : Starting discovery... 81 | 2020-06-22 13:34:24,429 INFO : Found 2 BLE-devices 82 | 2020-06-22 13:34:24,430 INFO : Trying to connect... 83 | 2020-06-22 13:34:24,464 INFO : [regulator] Connecting to d4:36:39:xx:xx:xx 84 | 2020-06-22 13:34:24,836 INFO : [regulator] Connected to BT-TH-39xxxxxx 85 | 2020-06-22 13:34:24,836 INFO : [regulator] Resolved services 86 | (...) 87 | 2020-06-22 13:34:24,843 INFO : [regulator] Found dev notify char [0000fff1-0000-1000-8000-00805f9b34fb] 88 | 2020-06-22 13:34:24,843 INFO : [regulator] Subscribing to notify char [0000fff1-0000-1000-8000-00805f9b34fb] 89 | 2020-06-22 13:34:24,843 INFO : [regulator] Found dev write char [0000ffd1-0000-1000-8000-00805f9b34fb] 90 | 2020-06-22 13:34:24,844 INFO : [regulator] Subscribing to notify char [0000ffd1-0000-1000-8000-00805f9b34fb] 91 | 2020-06-22 13:34:24,847 INFO : [battery_1] Connecting to 7c:01:0a:xx:xx:xx 92 | 2020-06-22 13:34:25,147 INFO : [battery_1] Connected to 12V100Ah-027 93 | 2020-06-22 13:34:25,148 INFO : [battery_1] Resolved services 94 | (...) 95 | 2020-06-22 13:34:25,155 INFO : [battery_1] Found dev notify char [0000ffe4-0000-1000-8000-00805f9b34fb] 96 | 2020-06-22 13:34:25,155 INFO : [battery_1] Subscribing to notify char [0000ffe4-0000-1000-8000-00805f9b34fb] 97 | 2020-06-22 13:34:25,155 INFO : Terminate with Ctrl+C 98 | (...) 99 | 2020-06-22 13:34:27,431 INFO : [regulator] Sending new data current: 0.5 100 | 2020-06-22 13:34:27,432 INFO : [regulator] Sending new data charge_current: 1.8 101 | 2020-06-22 13:34:27,433 INFO : [regulator] Sending new data voltage: 13.4 102 | 2020-06-22 13:34:27,434 INFO : [regulator] Sending new data charge_voltage: 13.4 103 | 2020-06-22 13:34:27,435 INFO : [regulator] Sending new data power: 7.0 104 | 2020-06-22 13:34:27,436 INFO : [regulator] Sending new data soc: 100.0 105 | 2020-06-22 13:34:27,438 INFO : [battery_1] Value of state changed from None to charging 106 | 2020-06-22 13:34:27,438 INFO : [battery_1] Value of health changed from None to perfect 107 | 2020-06-22 13:34:27,439 INFO : [battery_1] Sending new data current: 0.9 108 | 2020-06-22 13:34:27,440 INFO : [battery_1] Sending new data voltage: 13.6 109 | 2020-06-22 13:34:27,442 INFO : [battery_1] Sending new data power: 0.0 110 | ``` 111 | Updates can be sent to a remote server using either MQTT or JSON over HTTP(s) 112 | 113 | 114 | ## MQTT 115 | By using MQTT you will also get a listener for each topic, that can be used to set certain parameteres 116 | E.g. the app is sending MQTT states as 117 | `prefix/regulator/power_switch_state/state = 0` 118 | 119 | And you can turn power on and off by sending 120 | `prefix/regulator/power_switch_state/set = 1` 121 | from another MQTT client connected to the broker. *So do NOT connect to public brokers!* 122 | 123 | The MQTT-implemetation will automatically create sensors and switches in Home Assistant according to this spec: https://www.home-assistant.io/docs/mqtt/discovery/ 124 | 125 | ## JSON 126 | The data will be posted as JSON to a given URL as an object: 127 | ``` 128 | {"device": "battery_1", "current": -0.5, "ts": "2020-04-19 21:36:55"} 129 | {"device": "battery_1", "state": "discharging", "ts": "2020-04-19 21:36:55"} 130 | {"device": "regulator", "power_switch_state": 0, "ts": "2020-04-19 21:36:56"} 131 | {"device": "battery_1", "current": 0.0, "ts": "2020-04-19 21:36:56"} 132 | {"device": "battery_1", "state": "standby", "ts": "2020-04-19 21:36:57"} 133 | {"device": "battery_1", "capacity": 105.1, "ts": "2020-04-19 21:41:26"} 134 | ``` 135 | 136 | This allows you to remotely monitor the data from your installation: 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | # Credits 145 | A huge thanks to Pramod P K https://github.com/prapkengr/ for doing reverse engineering and decompiling of the Android Apps to figure out the protocols used. 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /REVERSE-ENGNEER.md: -------------------------------------------------------------------------------- 1 | # Reverse engineering the protocols 2 | 3 | Many of these devices use different protocols between the device and the app. 4 | They might be similar, but it seems lke every vendor want to invent the wheel, and to be frank - the wheels have a varying degree of roundness... 5 | 6 | Something is documented, something can be found in other github-repos where someone else has done the same, and some must just be guessed. 7 | 8 | But after working woth some of the devices for a while, you learn what to look for, and how to interpret the different data. 9 | 10 | ## Requirements 11 | The easiest way to start is to use an Android phone (maybe an iPhone, I have no idea how those work), the app for the device you want to reverse engieer, a computer with Wireshark and a USB cable for your phone. 12 | 13 | ## To begin 14 | * Install wireshark on your computer 15 | * Install the app on your phone and make sure it connects to the device. Hopefully, there are no passwords or pin codes or other things you need to worry about 16 | * Enable bluetooth-debugging (HCI snooping) on your phone. 17 | * * Exactly how this is done varies from model to model and different Android versions. Google it. 18 | * Start the app again 19 | * Look a bit around. Take some notes of the values you see (Voltage, current, temperature, charge cycles...). Also note down the timestamp 20 | * Leave everything for a minute or so to keep some steady values. 21 | * Connect a charger. Note the timestamp. Check what the app reports of charge power, changes in voltage etc. 22 | * Leave everything for a minute or so to keep some steady values. 23 | * Disconnect the charger. Note down new values from the app, along with the timestamp of the disconnect. 24 | * Leave everything for a minute or so to keep some steady values. 25 | * Connect some load - some lamps, something whatever that will draw power from the battery 26 | * Note down the values from the app and the timestamps 27 | * Disconnect the load 28 | * Stop the app 29 | * Disable bluetooth debugging on your phone and retrieve the bluetooth logs 30 | * * Exactly how this is done varies from model to model and different Android versions. Google it. 31 | 32 | 33 | ## Interpreting the data 34 | Now you should have a dump of the bluetooth traffic. 35 | 36 | Start by opening the bluetooth-dump in Wireshark. 37 | 38 | Create a filter with `bluetooth.addr=xx:xx:xx:xx:xx` (the mac-address of your device) 39 | 40 | Now you take out your notes and start looking for patterns. 41 | 42 | First you start looking whether there are typical "Send Write Command", followed by one or more "Rcvd Handle Value Notification", or if there are just a bunch of "Rcvd Handle Value Notification" 43 | - If you see a lot of Write Commands, it probably means that your device needs polling. 44 | - Look at the write command packets, and see if they repeat in some kind of pattern. There will typically be 2-3-4-5 different Write Commands that are repeated at regular intervals. 45 | - In Wireshark, you can then find the data that is sent, and note it down. 46 | 47 | In the "Value Notifications", you can find the data from the device. In Wireshark you will typically see the data as hex-values, use a hex-calculator (can easily be found online if you dont have one), and try out the different values. 48 | 49 | If you have multiple Value Notificatons following each other within a short timeframe, or just after a "Write Command", they probably belong together, so you should add them together as one string. 50 | 51 | For example, if you have received the following hex stream: `01034c0d010d050d030d01ee49ee49ee49ee49ee49ee49ee49ee49ee49ee49ee49ee49ee49ee49ee49ee49ee49ee49ee49ee49ee49ee49ee49ee49ee49ee49ee49ee490d050d0100020001000405338ad0` split it up into bytes: 52 | 53 | `01 03 4c 0d 01 0d 05 0d 03 0d 01 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 0d 05 0d 01 00 02 00 01 00 04 05 33 8a d0` 54 | 55 | One thing to remeber is that quite often, the Notifications will begin with some kind of ID, there is a big chance one of the first bytes represents the "length" of the string, and there is usually some kind of checksum at the end. 56 | 57 | So lets check this out. From my dump, I can easily see that all the Notifications starts with "0103", so that is probably some kind of ID or "Beginning of message" code. 58 | 59 | What about `4c`? 60 | 61 | `4C` = 76 - and look. The message is 81 bytes long...Two bytes header, the length-field is 1 byte, and maybe the last two bytes are a checksum? 62 | 63 | Let's check https://crccalc.com/ 64 | 65 | Dump all the bytes except the last two into the text field, select "Input: HEX" and click "CRC-16" 66 | 67 | CRC16/MODBUS comes back as "0xD08A", while the last two bytes in the tring si `8ad0`. It's the same bytes, but in the opposite order - but that is just big/little endian, so we are pretty sure those last two bytes are a checksum. 68 | 69 | Next we have a series of `0d 01 0d 05 0d 03 0d 01` what are those? 70 | 71 | `0D` = 13, could this be the voltage? 72 | `01` = 1, so if the reading from the app was 13.1, that could be it? 73 | 74 | But we have 0d 05 - 13 and 5? And then 0d 03 - 13 and 3, and then 0d 01 again. So it looks a bit strange. 75 | 76 | What if these are two byte long values? 77 | 78 | `0D01` is 3329 - wasn't that exactly what the value for Voltage in Cell 1? 79 | `0D05` is 3333 and `0D03` is 3331 - They to matches the Voltage in Cell 2 and 3. And 4 was 3329 again. We are on to something! 80 | 81 | Note this, and check if these matches also in the later packet dumps when we have connected a charger and some load. 82 | 83 | The we get a bunch of `ee 49`. So many repetitions? Probably some kind of filler. Nothing to worry about for now. 84 | 85 | The we get to the end: `...0d 05 0d 01 00 02 00 01 00 04 05 33 8a d0` 86 | 87 | 0D05 and 0D01 looks kind of like a repetition of Cell 2 and 4 or something? Let's see if they change in later packets. 88 | 89 | `00 01`, `00 02` and `00 04` might have some meaning. Some kind of status? Let's see if they change in later packets. 90 | 91 | `05` .. maybe? 92 | 93 | But `05 31` - this equals 1331, and lo! The app showed 13.31 V. 94 | 95 | Quite happy so far. Lets verify the cell data and voltage in the later packets. 96 | 97 | ### Another Notification: 98 | 99 | `010332025800000000000000000000026c00000000026c026c0258000000000064006430d330d430d4000600000000000000000000a1c2` 100 | 101 | 102 | `01 03 32 02 58 00 00 00 00 00 00 00 00 00 00 02 6c 00 00 00 00 02 6c 02 6c 02 58 00 00 00 00 00 64 00 64 30 d3 30 d4 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 a1 c2` 103 | 104 | Ok, again 0103 in the beginning. Nice. 105 | 106 | Lets verify the checksum again. Use the online tool, and check that CRC16/MODBUS comes back as `C2A1`. Different byte order, same bytes. We're happy with that. 107 | 108 | `32` = 50 and the packet is 55 bytes long. So that confirms 2 byte ID, 1 byte length 50 bytes of data and 2 bytes checksum. 109 | 110 | Let's look at the data again: 111 | 112 | * `02` is a 2. Could be anything. 113 | * `58` = 88 - Could be the SOC? Does it match the SOC in the app? 114 | * `0258` = 600 - Does not ring a bell 115 | * `5800` = 22582 - Not matching anything in the app... 116 | 117 | A bunch of 0. Let's just check if any of these change when we start charging or using the battery 118 | 119 | * `026C` = 620. Nah 120 | * `6C` = 108. Can't find anything there. 121 | * `6C00` = 27648. Still nothing. 122 | * `6C02` = 27650 ... 123 | 124 | Then this 0258 comes again. But still unknwon 125 | 126 | More zeros. But then a 64. 127 | 128 | `64` = 100, and the app reported 100% SOC. Could be worth looking at. 129 | 130 | One more `64` and then `30D3` 131 | 132 | `30D3` is 12499 - Woho! Didn't the app just report that the battery currently had 124.99 Ah? 133 | And that means that `30D4` = 12500, and the total capacity of the battery is 125 Ah. This looks promising! 134 | 135 | Just after those, we have a `06` and the app did report 6 charge cycles. Lets not this down and keep an eye on it after a few charges. 136 | 137 | And then just 0 until the checksum. 138 | 139 | ### Summary 140 | 141 | This is a very simple example. I have seen devices that use far more complex protocols than just reading the values directly. See for instance the [Meritsun plugin](https://github.com/Olen/solar-monitor/blob/07d39817d3f345994e886ebea3bdb830234820d3/plugins/Meritsun/__init__.py#L25) in this project. 142 | 143 | And even for this, for instance the temperature is quite hard to figure out. It is actually hidden in the "0258" in the beginning of the last Notification, 144 | 145 | It turns out you need to read the 2 bytes `0258`, which equals 600. Then you need to subtract 380, and you get 220, which is 22°C. This means that it will report values from `00 00` - Which is -38°C, and that 0°C is `01 7C`. 146 | 147 | I found this after placing the battery outdoors for a few hours, and noticing that these values changed, and then it was just a matter of trying to match these values with that the app reported. 148 | 149 | But it is hopefully a simple guide to get you started. 150 | 151 | Good luck. 152 | 153 | 154 | -------------------------------------------------------------------------------- /datalogger.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | import time 3 | import logging 4 | import json 5 | import requests 6 | import paho.mqtt.client as paho 7 | import socket 8 | 9 | 10 | 11 | 12 | class DataLoggerMqtt(): 13 | def __init__(self, broker, port, prefix=None, username=None, password=None, hostname=None): 14 | logging.debug("Creating new MQTT-logger") 15 | if prefix == None: 16 | prefix = "solar-monitor" 17 | self.broker = broker 18 | if not hostname: 19 | hostname = socket.gethostname() 20 | 21 | self.client = paho.Client(paho.CallbackAPIVersion.VERSION1, "{}".format(hostname)) # create client object 22 | if username and password: 23 | self.client.username_pw_set(username=username,password=password) 24 | 25 | self.client.on_publish = self.on_publish # assign function to callback 26 | self.client.on_message = self.on_message # attach function to callback 27 | self.client.on_subscribe = self.on_subscribe # attach function to callback 28 | self.client.on_log = self.on_log 29 | 30 | self.client.connect(broker, port) # establish connection 31 | self.client.loop_start() # start the loop 32 | 33 | self.sensors = [] 34 | self.sets = {} 35 | if not prefix.endswith("/"): 36 | prefix = prefix + "/" 37 | self._prefix = prefix 38 | self.trigger = {} 39 | self._listener_created = {} 40 | 41 | @property 42 | def prefix(self): 43 | return self._prefix 44 | 45 | @prefix.setter 46 | def prefix(self, val): 47 | if not val.endswith("/"): 48 | val = val + "/" 49 | self._prefix = val 50 | 51 | def publish(self, device, var, val, refresh=False): 52 | topic = "{}{}/{}/state".format(self.prefix, device, var) 53 | if topic not in self.sensors or refresh: 54 | if "power_switch" in var: 55 | # self.delete_switch(device, var) 56 | # time.sleep(1) 57 | self.create_switch(device, var) 58 | self.create_listener(device, var) 59 | else: 60 | if not refresh: 61 | self.delete_sensor(device, var) 62 | time.sleep(0.5) 63 | self.create_sensor(device, var) 64 | self.sensors.append(topic) 65 | time.sleep(0.1) 66 | logging.debug("Publishing to MQTT {}: {} = {}".format(self.broker, topic, val)) 67 | ret = self.client.publish(topic, val, retain=True) 68 | if "power_switch" in var and time.time() > self._listener_created[device, var] + 300: 69 | self.create_listener(device, var) 70 | 71 | def create_switch(self, device, var): 72 | topic = "{}{}/{}/state".format(self.prefix, device, var) 73 | logging.debug("Creating MQTT-switch {}".format(topic)) 74 | ha_topic = "homeassistant/switch/{}/{}/config".format(device, var) 75 | val = { 76 | "name": "{} {} {}".format(self.prefix[:-1].capitalize(), device.replace("_", " ").title(), var.replace("_", " ").title()), 77 | "unique_id": "{}_{}_{}".format(self.prefix[:-1], device, var), 78 | "state_topic": topic, 79 | "command_topic": "{}{}/{}/set".format(self.prefix, device, var), 80 | "payload_on": 1, 81 | "payload_off": 0, 82 | } 83 | ret = self.client.publish(ha_topic, json.dumps(val), retain=True) 84 | 85 | 86 | def create_sensor(self, device, var): 87 | topic = "{}{}/{}/state".format(self.prefix, device, var) 88 | logging.debug("Creating MQTT-sensor {}".format(topic)) 89 | ha_topic = "homeassistant/sensor/{}/{}/config".format(device, var) 90 | val = { 91 | "name": "{} {} {}".format(self.prefix[:-1].capitalize(), device.replace("_", " ").title(), var.replace("_", " ").title()), 92 | "unique_id": "{}_{}_{}".format(self.prefix[:-1], device, var), 93 | "state_topic": topic, 94 | "force_update": True, 95 | } 96 | if var == "temperature": 97 | val['device_class'] = "temperature" 98 | val['unit_of_measurement'] = "°C" 99 | elif var == "soc": 100 | val['device_class'] = "battery" 101 | val['unit_of_measurement'] = "%" 102 | elif var == "power" or var == "charge_power" or var == "input_power": 103 | val['device_class'] = "power" 104 | val['unit_of_measurement'] = "W" 105 | elif var == "voltage" or var == "charge_voltage" or var == "input_voltage": 106 | val['device_class'] = "voltage" 107 | val['icon'] = "mdi:flash" 108 | val['unit_of_measurement'] = "V" 109 | elif var == "current" or var == "charge_current" or var == "input_current": 110 | val['device_class'] = "current" 111 | val['icon'] = "mdi:current-dc" 112 | val['unit_of_measurement'] = "A" 113 | elif var.endswith("_state"): 114 | val['device_class'] = "enum" 115 | val['options'] = ["charging", "standby", "discharging"] 116 | elif var == "charge_cycles": 117 | val['icon'] = "mdi:recycle" 118 | elif var == "health": 119 | val['icon'] = "mdi:heart-flash" 120 | elif "battery" in device and "cell" in var: 121 | val['icon'] = "mdi:battery" 122 | val['unit_of_measurement'] = "mV" 123 | val['device_class'] = "voltage" 124 | elif "battery" in device: 125 | val['icon'] = "mdi:battery" 126 | elif "regulator" in device: 127 | val['icon'] = "mdi:solar-power" 128 | elif "inverter" in device: 129 | val['icon'] = "mdi:current-ac" 130 | elif "rectifier" in device: 131 | val['icon'] = "mdi:current-ac" 132 | 133 | ret = self.client.publish(ha_topic, json.dumps(val), retain=True) 134 | 135 | def delete_switch(self, device, var): 136 | ha_topic = "homeassistant/switch/{}/{}/config".format(device, var) 137 | ret = self.client.publish(ha_topic, payload=None) 138 | 139 | def delete_sensor(self, device, var): 140 | ha_topic = "homeassistant/sensor/{}/{}/config".format(device, var) 141 | ret = self.client.publish(ha_topic, payload=None) 142 | 143 | def create_listener(self, device, var): 144 | topic = "{}{}/{}/set".format(self.prefix, device, var) 145 | logging.info("Creating MQTT-listener {}".format(topic)) 146 | try: 147 | self.client.subscribe((topic, 0)) 148 | self._listener_created[device, var] = time.time() 149 | except Exception as e: 150 | logging.error("MQTT: {}".format(e)) 151 | self.sets[device] = [] 152 | 153 | 154 | 155 | def on_publish(self, client, userdata, result): #create function for callback 156 | logging.debug("Published to MQTT") 157 | 158 | def on_subscribe(self, client, userdata, mid, granted_qos): 159 | # logging.debug("Subscribed to MQTT topic {}".format(userdata)) 160 | pass 161 | 162 | 163 | def on_message(self, client, userdata, message): 164 | topic = message.topic 165 | payload = message.payload.decode("utf-8") 166 | logging.info("MQTT message received {}".format(str(message.payload.decode("utf-8")))) 167 | logging.debug("MQTT message topic={}".format(message.topic)) 168 | logging.debug("MQTT message qos={}".format(message.qos)) 169 | logging.debug("MQTT message retain flag={}".format(message.retain)) 170 | (prefix, device, var, crap) = topic.split("/") 171 | self.sets[device].append((var, payload)) 172 | logging.info("MQTT set: {}".format(self.sets)) 173 | if self.trigger[device]: 174 | self.trigger[device].set() 175 | 176 | def on_log(self, client, userdata, level, buf): 177 | logging.debug("MQTT {}".format(buf)) 178 | 179 | 180 | # 2021-07-07 17:46:22,051 INFO : MQTT message received 1 181 | # 2021-07-07 17:46:22,052 INFO : MQTT set: {'regulator': [('power_switch', '1')], 'battery_1': [], 'battery_2': [], 'inverter_1': []} 182 | # 2021-07-07 17:46:22,054 INFO : [regulator] MQTT-poller-thread regulator Event happened... 183 | # 2021-07-07 17:46:22,055 INFO : [regulator] MQTT-msg: power_switch -> 1 184 | # 2021-07-07 17:46:22,136 INFO : [battery_2] Sending new data current: -1.4 185 | # 2021-07-07 17:46:22,156 INFO : [inverter_1] Sending new data voltage: 230.0 186 | # 2021-07-07 17:46:22,260 WARNING : [regulator] Write to characteristic failed for: [0000ffd1-0000-1000-8000-00805f9b34fb] with error [In Progress] 187 | 188 | 189 | class DataLogger(): 190 | def __init__(self, config): 191 | # config.get('datalogger', 'url'), config.get('datalogger', 'token') 192 | logging.debug("Creating new DataLogger") 193 | self.url = None 194 | self.mqtt = None 195 | if config.get('datalogger', 'url', fallback=None): 196 | self.url = config.get('datalogger', 'url') 197 | self.token = config.get('datalogger', 'token') 198 | if config.get('mqtt', 'broker', fallback=None): 199 | self.mqtt = DataLoggerMqtt( 200 | config.get('mqtt', 'broker'), 201 | config.get('mqtt', 'port', fallback=1883), 202 | prefix=config.get('mqtt', 'prefix', fallback=None), 203 | username=config.get('mqtt', 'username', fallback=None), 204 | password=config.get('mqtt', 'password', fallback=None), 205 | hostname=config.get('mqtt', 'hostname', fallback=None) 206 | ) 207 | self.logdata = {} 208 | 209 | 210 | # logdata 211 | # - device_id 212 | # var1: 213 | # ts: timestamp 214 | # value: value 215 | # 216 | # 217 | # } 218 | 219 | def log(self, device, var, val): 220 | # Only log modified data 221 | # : 222 | device = device.strip() 223 | # ts = datetime.now().isoformat(' ', 'seconds') 224 | ts = datetime.now() 225 | logging.debug("[{}] All data {}: {}".format(device, var, val)) 226 | if device not in self.logdata: 227 | self.logdata[device] = {} 228 | if var not in self.logdata[device]: 229 | self.logdata[device][var] = {} 230 | self.logdata[device][var]['ts'] = ts 231 | self.logdata[device][var]['value'] = None 232 | 233 | if self.logdata[device][var]['value'] != val: 234 | self.logdata[device][var]['ts'] = ts 235 | self.logdata[device][var]['value'] = val 236 | logging.info("[{}] Sending new data {}: {}".format(device, var, val)) 237 | self.send_to_server(device, var, val) 238 | elif self.logdata[device][var]['ts'] < datetime.now()-timedelta(minutes=10): 239 | self.logdata[device][var]['ts'] = ts 240 | self.logdata[device][var]['value'] = val 241 | # logging.debug("Sending data to server due to long wait") 242 | logging.info("[{}] Sending refreshed data {}: {}".format(device, var, val)) 243 | self.send_to_server(device, var, val, True) 244 | 245 | 246 | 247 | 248 | def send_to_server(self, device, var, val, refresh=False): 249 | if self.mqtt: 250 | self.mqtt.publish(device, var, val, refresh) 251 | if self.url: 252 | logging.info("[{}] Sending data to {}".format(device, self.url)) 253 | ts = datetime.now().isoformat(' ', 'seconds') 254 | payload = {'device': device, var: val, 'ts': ts} 255 | header = {'Content-type': 'application/json', 'Accept': 'text/plain', 'Authorization': 'Bearer {}'.format(self.token)} 256 | try: 257 | response = requests.post(url=self.url, json=payload, headers=header) 258 | except TimeoutError: 259 | logging.error("Connection to {} timed out!".format(self.url)) 260 | 261 | 262 | 263 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | solar-monitor: 3 | build: 4 | context: . 5 | network: host 6 | container_name: solar-monitor 7 | network_mode: host 8 | volumes: 9 | - /var/run/dbus:/var/run/dbus 10 | - ~/solar-monitor/solar-monitor.ini:/solar-monitor/solar-monitor.ini 11 | - ~/solar-monitor/logs:/solar-monitor/solar-monitor 12 | restart: unless-stopped 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /duallog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Duallog 4 | 5 | Based on the original https://github.com/acschaefer/duallog 6 | Contains quite a few fixes and modifications 7 | 8 | This module contains a function "setup()" that sets up dual logging. 9 | All subsequent log messages are sent both to the console and to a logfile. 10 | Log messages are generated via the "logging" package. 11 | 12 | Example: 13 | >>> import duallog 14 | >>> import logging 15 | >>> duallog.setup('mylogs') 16 | >>> logging.info('Test message') 17 | 18 | If run, this module illustrates the usage of the duallog package. 19 | """ 20 | 21 | 22 | # Import required standard packages. 23 | import datetime 24 | import logging.handlers 25 | import os 26 | 27 | # Define default logfile format. 28 | file_name_format = '{year:04d}{month:02d}{day:02d}-'\ 29 | '{hour:02d}{minute:02d}{second:02d}.log' 30 | 31 | # Define the default logging message formats. 32 | file_msg_format = '%(asctime)s %(levelname)-8s: %(message)s' 33 | console_msg_format = '%(levelname)s: %(message)s' 34 | 35 | # Define the log rotation criteria. 36 | max_bytes=1024**2 37 | backup_count=100 38 | 39 | 40 | def setup(dir='log', minLevel=logging.WARNING, rotation='size', keep=backup_count, fileLevel=logging.DEBUG): 41 | """ Set up dual logging to console and to logfile. 42 | 43 | When this function is called, it first creates the given logging output directory. 44 | It then creates a logfile and passes all log messages to come to it. 45 | The name of the logfile encodes the date and time when it was created, for example "20181115-153559.log". 46 | All messages with a certain minimum log level are also forwarded to the console. 47 | 48 | Args: 49 | dir: path of the directory where to store the log files. Both a 50 | relative or an absolute path may be specified. If a relative path is 51 | specified, it is interpreted relative to the working directory. 52 | Defaults to "log". 53 | minLevel: defines the minimum level of the messages that will be shown on the console. Defaults to WARNING. 54 | """ 55 | 56 | # Create the root logger. 57 | logger = logging.getLogger() 58 | logger.setLevel(logging.DEBUG) 59 | 60 | # Validate the given directory. 61 | dir = os.path.normpath(dir) 62 | 63 | # Create a folder for the logfiles. 64 | if not os.path.exists(dir): 65 | os.makedirs(dir) 66 | 67 | # Construct the name of the logfile. 68 | t = datetime.datetime.now() 69 | file_name = file_name_format.format(year=t.year, month=t.month, day=t.day, 70 | hour=t.hour, minute=t.minute, second=t.second) 71 | file_name = os.path.join(dir, file_name) 72 | 73 | # Set up logging to the logfile. 74 | if rotation == 'daily': 75 | file_name = "{}.log".format(dir) 76 | file_name = os.path.join(dir, file_name) 77 | file_handler = logging.handlers.TimedRotatingFileHandler( 78 | filename=file_name, when='midnight', backupCount=keep) 79 | elif rotation == 'hourly': 80 | file_name = "{}.log".format(dir) 81 | file_name = os.path.join(dir, file_name) 82 | file_handler = logging.handlers.TimedRotatingFileHandler( 83 | filename=file_name, when='H', backupCount=keep) 84 | else: 85 | file_handler = logging.handlers.RotatingFileHandler( 86 | filename=file_name, maxBytes=max_bytes, backupCount=keep) 87 | 88 | file_handler.setLevel(fileLevel) 89 | file_formatter = logging.Formatter(file_msg_format) 90 | file_handler.setFormatter(file_formatter) 91 | logger.addHandler(file_handler) 92 | 93 | # Set up logging to the console. 94 | stream_handler = logging.StreamHandler() 95 | stream_handler.setLevel(minLevel) 96 | stream_formatter = logging.Formatter(console_msg_format) 97 | stream_handler.setFormatter(stream_formatter) 98 | logger.addHandler(stream_handler) 99 | 100 | 101 | if __name__ == '__main__': 102 | """Illustrate the usage of the duallog package. 103 | """ 104 | 105 | # Set up dual logging. 106 | setup('logtest') 107 | 108 | # Generate some log messages. 109 | logging.debug('Debug messages are only sent to the logfile.') 110 | logging.info('Info messages are not shown on the console, too.') 111 | logging.warning('Warnings appear both on the console and in the logfile.') 112 | logging.error('Errors get the same treatment.') 113 | logging.critical('And critical messages, of course.') 114 | -------------------------------------------------------------------------------- /img/Battery-Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Olen/solar-monitor/d2f84377e027af41e7e37e82047f81882165fa74/img/Battery-Screenshot.png -------------------------------------------------------------------------------- /img/SRNE-Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Olen/solar-monitor/d2f84377e027af41e7e37e82047f81882165fa74/img/SRNE-Screenshot.png -------------------------------------------------------------------------------- /plugins/Hacien/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # from __future__ import print_function 4 | import os 5 | import sys 6 | import time 7 | from datetime import datetime 8 | 9 | import logging 10 | 11 | 12 | class Config(): 13 | SEND_ACK = False 14 | NEED_POLLING = True 15 | NOTIFY_SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e" 16 | NOTIFY_CHAR_UUID = "6e400003-b5a3-f393-e0a9-e50e24dcca9e" 17 | WRITE_SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e" 18 | WRITE_CHAR_UUID_POLLING = "6e400002-b5a3-f393-e0a9-e50e24dcca9e" 19 | 20 | class Util(): 21 | ''' 22 | Class for reading and parsing data from various Hacien-batteries (App: HC BLE) 23 | 24 | Reading values is pretty straight forward 25 | The batteries need to be polled, polling strings from the app is easily found 26 | Values are simple 1 or 2 byte integers, little endian, so they can be read more or less directly 27 | 28 | ''' 29 | 30 | def __init__(self, power_device): 31 | self.SOI = 0x0103 32 | self.INFO = None 33 | self.EOI = None 34 | self.buffer = [] 35 | self.pollnum = 0 36 | self.RecvDataType = self.SOI 37 | self.RevBuf = [None] * 122 38 | self.Revindex = 0 39 | # self.TAG = "SmartPowerUtil" 40 | self.PowerDevice = power_device 41 | self.end = 0 42 | self.prev_values = {} 43 | 44 | 45 | def validate(self, msg: list) -> bool: 46 | return len(msg) > 0 and self.modbusCrc(msg) == 0 47 | 48 | def modbusCrc(self, msg: list) -> int: 49 | crc = 0xFFFF 50 | for n in msg: 51 | crc ^= n 52 | for i in range(8): 53 | if crc & 1: 54 | crc >>= 1 55 | crc ^= 0xA001 56 | else: 57 | crc >>= 1 58 | return crc 59 | 60 | def getValue(self, buf: bytearray, start: int, length: int = 1) -> int: 61 | ''' Reads length bytes from buf ''' 62 | if length == 1: 63 | return int(buf[start]) 64 | if length == 2: 65 | return int(buf[start]*256 + buf[start + 1]) 66 | return 0 67 | 68 | 69 | 70 | def notificationUpdate(self, data, char): 71 | logging.debug("broadcastUpdate Start {} {}".format(data, data.hex())) 72 | if self.PowerDevice.config.getboolean('monitor', 'debug', fallback=False): 73 | with open(f"/tmp/{self.PowerDevice.alias()}.log", 'a') as debugfile: 74 | debugfile.write(f"{datetime.now()} <- {data.hex()}\n") 75 | if data[0] == 1 and data[1] == 3: 76 | # New message 77 | self.buffer = [] 78 | for char in data: 79 | self.buffer.append(char) 80 | if self.validate(self.buffer): 81 | # The checksum is correct, so we assume we have a complete message 82 | return self.handleMessage(self.buffer) 83 | return False 84 | 85 | def handleMessage(self, message): 86 | # Accepts a list of hex-characters, and returns the human readable values into the powerDevice object 87 | if message == None or "" == message: 88 | return False 89 | logging.debug("handleMessage {}".format(message)) 90 | if message[2] in self.prev_values: 91 | if message != self.prev_values[message[2]]: 92 | logging.debug("Response changed:") 93 | logging.debug(f"- {self.prev_values[message[2]]}") 94 | logging.debug(f"+ {message}:") 95 | self.prev_values[message[2]] = message 96 | 97 | # logging.debug("test handleMessage == {}".format(message)) 98 | self.PowerDevice.entities.msg = message 99 | ''' 100 | Fortunately we read a different number of bytes from each register, so we can 101 | abuse the "length" field (byte #3 in the response) as an "id" 102 | ''' 103 | 104 | if len(message) > 10 and message[2] == 0x32: 105 | 106 | # DEBUG: broadcastUpdate Start b'\x01\x032\x01\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xea\x00\x00' 01033201e00000000000000000000001ea0000 107 | # DEBUG: broadcastUpdate Start b'\x00\x00\x02\x12\x01\xea\x01\xe0\x00G\x00\x00\x00d\x00d0\xd40\xd4' 0000021201ea01e0004700000064006430d430d4 108 | # DEBUG: broadcastUpdate Start b'0\xd4\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\xa6' 30d400060000000000000000000004a6 109 | # 110 | # 0103 32 01e0 00 00 00 00 00 00 00 00 00 00 01 ea 00 00 111 | # 0000 02 12 01 ea 01e0 0047 00000064006430d430d4 112 | # 30d400060000000000000000000004a6 113 | # 114 | # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 115 | # DEBUG: - [1, 3, 50, 1, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 234, 0, 0, 0, 0, 2, 18, 1, 234, 1, 224, 0, 0, 0, 0, 0, 100, 0, 100, 48, 212, 48, 212, 48, 212, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 248, 29] 116 | # DEBUG: + [1, 3, 50, 1, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 234, 0, 0, 0, 0, 2, 18, 1, 234, 1, 224, 0, 71, 0, 0, 0, 100, 0, 100, 48, 212, 48, 212, 48, 212, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 166]: 117 | # 118 | 119 | 120 | 121 | self.PowerDevice.entities.temperature_celsius = int(((message[3]*256 + message[4]) - 380) / 10) 122 | # Charge 123 | charge_current = (message[27]*256 + message[28]) * 100 124 | # Usage 125 | draw_current = (message[29]*256 + message[30]) * 100 126 | if draw_current > 0: 127 | self.PowerDevice.entities.mcurrent = draw_current * -1 128 | else: 129 | self.PowerDevice.entities.mcurrent = charge_current 130 | self.PowerDevice.entities.exp_capacity = (message[35]*256 + message[36]) / 100 131 | self.PowerDevice.entities.mcapacity = (message[37]*256 + message[38]) * 10 132 | # print(message[37]*256 + message[38]) 133 | # self.PowerDevice.entities.max_capacity = message[37]*256 + message[38] 134 | self.PowerDevice.entities.charge_cycles = message[42] 135 | self.PowerDevice.entities.soc = message[32] 136 | self.PowerDevice.entities.battery_temperature_celsius = self.PowerDevice.entities.temperature_celsius 137 | return True 138 | 139 | # current_ah = message[35]*256 + message[36] 140 | # total_ah1 = message[37]*256 + message[38] 141 | # total_ah2 = message[39]*256 + message[40] 142 | # cycles = message[42] 143 | # print(use, soc1, soc2, current_ah, total_ah1, total_ah2, cycles) 144 | 145 | # temp1 = message[3]*256+buffer[4] 146 | # print("T1", temp1) 147 | # temp2 = temp1 - 380 148 | # print("T2", temp2) 149 | # temp3 = temp2 / 10 150 | # print("T3", temp3) 151 | elif len(message) > 10 and message[2] == 0x4c: 152 | self.PowerDevice.entities.mvoltage = (message[-4]*256 + message[-3]) * 10 153 | i = 0 154 | while i < 8: 155 | cellid = i / 2 156 | # print(i, cellid) 157 | # print(message[i + 3], message[i + 4]) 158 | if message[i + 3] != 238 and message[i + 4] != 73: 159 | self.PowerDevice.entities.cell_mvoltage = (int(cellid) + 1, message[i + 3]*256 + message[i + 4]) 160 | # print(self.PowerDevice.entities.cell_mvoltage) 161 | i = i + 2 162 | return True 163 | # No changes, so we return False 164 | return False 165 | 166 | 167 | def pollRequest(self, force = None): 168 | data = None 169 | if self.pollnum == 1: 170 | data = [0x01, 0x03, 0xd0, 0x00, 0x00, 0x26, 0xfc, 0xd0] 171 | elif self.pollnum == 2: 172 | data = [0x01, 0x03, 0xd0, 0x00, 0x00, 0x26, 0xfc, 0xd0] 173 | data = None 174 | elif self.pollnum == 3: 175 | data = [0x01, 0x03, 0xd0, 0x26, 0x00, 0x19, 0x5d, 0x0b] 176 | elif self.pollnum == 4: 177 | data = [0x01, 0x03, 0xd1, 0x15, 0x00, 0x0c, 0x6d, 0x37] 178 | data = None 179 | elif self.pollnum == 5: 180 | data = [0x01, 0x03, 0xd1, 0x00, 0x00, 0x15, 0xbd, 0x39] 181 | data = None 182 | elif self.pollnum == 6: 183 | data = [0x01, 0x03, 0x23, 0x1c, 0x00, 0x04, 0x8e, 0x4b] 184 | data = None 185 | elif self.pollnum == 7: 186 | data = [0x01, 0x03, 0xd2, 0x00, 0x00, 0x01, 0xbd ,0x72] 187 | data = None 188 | elif self.pollnum == 10: 189 | self.pollnum = 0 190 | self.pollnum = self.pollnum + 1 191 | if data and self.PowerDevice.config.getboolean('monitor', 'debug', fallback=False): 192 | with open(f"/tmp/{self.PowerDevice.alias()}.log", 'a') as debugfile: 193 | debugfile.write(f"{datetime.now()} -> {bytearray(data).hex()}\n") 194 | return data 195 | 196 | def ackData(self): 197 | return [] 198 | # [0x01, 0x03, 0xd0, 0x00, 0x00, 0x26, 0xfc, 0xd0] 199 | 200 | -------------------------------------------------------------------------------- /plugins/Hacien/dev/bms-2024-03-20-01.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Olen/solar-monitor/d2f84377e027af41e7e37e82047f81882165fa74/plugins/Hacien/dev/bms-2024-03-20-01.pcapng -------------------------------------------------------------------------------- /plugins/Hacien/dev/bms-2024-03-20.07.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Olen/solar-monitor/d2f84377e027af41e7e37e82047f81882165fa74/plugins/Hacien/dev/bms-2024-03-20.07.pcapng -------------------------------------------------------------------------------- /plugins/Hacien/dev/bms-2024-03-20.08.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Olen/solar-monitor/d2f84377e027af41e7e37e82047f81882165fa74/plugins/Hacien/dev/bms-2024-03-20.08.pcapng -------------------------------------------------------------------------------- /plugins/Hacien/dev/bms-session-2024-03-20-01.txt: -------------------------------------------------------------------------------- 1 | 2 | -> 01 03 23 1c 00 04 8e 4b 3 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 4 | 5 | -> 01 03 d2 00 00 01 bd 72 6 | <- 01 03 02 00 00 b8 44 7 | 8 | -> 01 03 d0 00 00 26 fc d0 9 | <- 01 03 4c 0c fe 0d 01 0d 00 0c fe ee 49 ee 49 ee 49 ee 49 10 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 11 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 12 | <- ee 49 ee 49 ee 49 ee 49 0d 01 0c fe 00 02 00 01 00 03 05 32 13 | <- dd 1f 14 | 15 | -> 01 03 d0 00 00 26 fc d0 16 | <- 01 03 4c 0c fe 0d 01 0d 00 0c fe ee 49 ee 49 ee 49 ee 49 17 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 18 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 19 | <- ee 49 ee 49 ee 49 ee 49 0d 01 0c fe 00 02 00 01 00 03 05 32 20 | <- dd 1f 21 | 22 | -> 01 03 d0 26 00 19 5d 0b 23 | <- 01 03 32 02 44 00 00 00 00 00 00 00 00 00 00 02 58 00 00 24 | <- 00 00 02 4e 02 58 02 44 00 00 00 00 00 64 00 64 30 d3 30 d4 25 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 64 4c 26 | 27 | 02 = 2 28 | 58 = 88 29 | 0258 = 600 30 | 6C = 108 31 | 026C = 620 32 | 6C = 108 33 | 026C = 620 34 | 6C = 108 35 | 026C = 620 36 | 58 = 88 37 | 0258 = 600 38 | 64 = 100 (SOC?) 39 | 30D3 = 12499 => Ah 40 | 30D4 = 12500 => Max Ah 41 | 30D4 = 12500 => Max Ah again? 42 | 06 = 6 -> Charge cycles? 43 | 44 | 0258 = 600 22 45 | 0244 = 580 20 - 20 46 | 0226 = 550 17 - 30 47 | 48 | 017C = 380 0 - 170 49 | 0000 = 0 -38 50 | 51 | 52 | 53 | 54 | 55 | -> 01 03 d1 15 00 0c 6d 37 56 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 57 | <- 00 00 00 00 00 00 00 00 bc 90 58 | 59 | -> 01 03 d1 00 00 15 bd 39 60 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 61 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 62 | <- 00 00 00 00 00 00 e8 5c 63 | 64 | -> 01 03 23 1c 00 04 8e 4b 65 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 66 | 67 | -> 01 03 d2 00 00 01 bd 72 68 | <- 01 03 02 00 00 b8 44 69 | 70 | -> 01 03 d0 00 00 26 fc d0 71 | <- 01 03 4c 0c fe 0d 01 0d 00 0c fe ee 49 ee 49 ee 49 ee 49 72 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 73 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 74 | <- ee 49 ee 49 ee 49 ee 49 0d 01 0c fe 00 02 00 01 00 03 05 32 75 | <- dd 1f 76 | 77 | -> 01 03 d0 00 00 26 fc d0 78 | <- 01 03 4c 0c fe 0d 01 0d 00 0c fe ee 49 ee 49 ee 49 ee 49 79 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 80 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 81 | <- ee 49 ee 49 ee 49 ee 49 0d 01 0c fe 00 02 00 01 00 03 05 32 82 | <- dd 1f 83 | 84 | -> 01 03 d0 26 00 19 5d 0b 85 | <- 01 03 32 02 44 00 00 00 00 00 00 00 00 00 00 02 58 00 00 86 | <- 00 00 02 4e 02 58 02 44 00 00 00 00 00 64 00 64 30 d3 30 d4 87 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 64 4c 88 | 89 | -> 01 03 d1 15 00 0c 6d 37 90 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 91 | <- 00 00 00 00 00 00 00 00 bc 90 92 | 93 | -> 01 03 d1 00 00 15 bd 39 94 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 95 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 96 | <- 00 00 00 00 00 00 e8 5c 97 | 98 | -> 01 03 23 1c 00 04 8e 4b 99 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 100 | 101 | -> 01 03 d2 00 00 01 bd 72 102 | <- 01 03 02 00 00 b8 44 103 | 104 | -> 01 03 d0 00 00 26 fc d0 105 | <- 01 03 4c 0c fe 0d 01 0d 00 0c fe ee 49 ee 49 ee 49 ee 49 106 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 107 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 108 | <- ee 49 ee 49 ee 49 ee 49 0d 01 0c fe 00 02 00 01 00 03 05 32 109 | <- dd 1f 110 | 111 | -> 01 03 d0 00 00 26 fc d0 112 | <- 01 03 4c 0c fe 0d 01 0d 00 0c fe ee 49 ee 49 ee 49 ee 49 113 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 114 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 115 | <- ee 49 ee 49 ee 49 ee 49 0d 01 0c fe 00 02 00 01 00 03 05 32 116 | <- dd 1f 117 | 118 | -> 01 03 d0 26 00 19 5d 0b 119 | <- 01 03 32 02 44 00 00 00 00 00 00 00 00 00 00 02 58 00 00 120 | <- 00 00 02 4e 02 58 02 44 00 00 00 00 00 64 00 64 30 d3 30 d4 121 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 64 4c 122 | 123 | -> 01 03 d1 15 00 0c 6d 37 124 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 125 | <- 00 00 00 00 00 00 00 00 bc 90 126 | 127 | -> 01 03 d1 00 00 15 bd 39 128 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 129 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 130 | <- 00 00 00 00 00 00 e8 5c 131 | 132 | -> 01 03 23 1c 00 04 8e 4b 133 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 134 | 135 | -> 01 03 d2 00 00 01 bd 72 136 | <- 01 03 02 00 00 b8 44 137 | 138 | -> 01 03 d0 00 00 26 fc d0 139 | <- 01 03 4c 0c fe 0d 01 0d 00 0c fe ee 49 ee 49 ee 49 ee 49 140 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 141 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 142 | <- ee 49 ee 49 ee 49 ee 49 0d 01 0c fe 00 02 00 01 00 03 05 32 143 | <- dd 1f 144 | 145 | -> 01 03 d0 00 00 26 fc d0 146 | <- 01 03 4c 0c fe 0d 01 0d 00 0c fe ee 49 ee 49 ee 49 ee 49 147 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 148 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 149 | <- ee 49 ee 49 ee 49 ee 49 0d 01 0c fe 00 02 00 01 00 03 05 32 150 | <- dd 1f 151 | 152 | -> 01 03 d0 26 00 19 5d 0b 153 | <- 01 03 32 02 44 00 00 00 00 00 00 00 00 00 00 02 58 00 00 154 | <- 00 00 02 4e 02 58 02 44 00 00 00 00 00 64 00 64 30 d3 30 d4 155 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 64 4c 156 | 157 | -> 01 03 d1 15 00 0c 6d 37 158 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 159 | <- 00 00 00 00 00 00 00 00 bc 90 160 | 161 | -> 01 03 d1 00 00 15 bd 39 162 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 163 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 164 | <- 00 00 00 00 00 00 e8 5c 165 | 166 | -> 01 03 23 1c 00 04 8e 4b 167 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 168 | 169 | -> 01 03 d2 00 00 01 bd 72 170 | <- 01 03 02 00 00 b8 44 171 | 172 | -> 01 03 d0 00 00 26 fc d0 173 | <- 01 03 4c 0c fe 0d 01 0d 00 0c fe ee 49 ee 49 ee 49 ee 49 174 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 175 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 176 | <- ee 49 ee 49 ee 49 ee 49 0d 01 0c fe 00 02 00 01 00 03 05 32 177 | <- dd 1f 178 | 179 | -> 01 03 d0 00 00 26 fc d0 180 | <- 01 03 4c 0c fe 0d 01 0d 00 0c fe ee 49 ee 49 ee 49 ee 49 181 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 182 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 183 | <- ee 49 ee 49 ee 49 ee 49 0d 01 0c fe 00 02 00 01 00 03 05 32 184 | <- dd 1f 185 | 186 | -> 01 03 d0 26 00 19 5d 0b 187 | <- 01 03 32 02 44 00 00 00 00 00 00 00 00 00 00 02 58 00 00 188 | <- 00 00 02 4e 02 58 02 44 00 00 00 00 00 64 00 64 30 d3 30 d4 189 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 64 4c 190 | 191 | -> 01 03 d1 15 00 0c 6d 37 192 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 193 | <- 00 00 00 00 00 00 00 00 bc 90 194 | 195 | -> 01 03 d1 00 00 15 bd 39 196 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 197 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 198 | <- 00 00 00 00 00 00 e8 5c 199 | 200 | -> 01 03 23 1c 00 04 8e 4b 201 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 202 | 203 | -> 01 03 d2 00 00 01 bd 72 204 | <- 01 03 02 00 00 b8 44 205 | 206 | -> 01 03 d0 00 00 26 fc d0 207 | <- 01 03 4c 0c fe 0d 01 0c ff 0c fe ee 49 ee 49 ee 49 ee 49 208 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 209 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 210 | <- ee 49 ee 49 ee 49 ee 49 0d 01 0c fe 00 02 00 01 00 03 05 32 211 | <- 6c db 212 | 213 | -> 01 03 d0 00 00 26 fc d0 214 | <- 01 03 4c 0c fe 0d 01 0d 00 0c fe ee 49 ee 49 ee 49 ee 49 215 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 216 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 217 | <- ee 49 ee 49 ee 49 ee 49 0d 01 0c fe 00 02 00 01 00 03 05 32 218 | <- dd 1f 219 | 220 | -> 01 03 d0 26 00 19 5d 0b 221 | <- 01 03 32 02 44 00 00 00 00 00 00 00 00 00 00 02 58 00 00 222 | <- 00 00 02 4e 02 58 02 44 00 00 00 00 00 64 00 64 30 d3 30 d4 223 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 64 4c 224 | 225 | -> 01 03 d1 15 00 0c 6d 37 226 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 227 | <- 00 00 00 00 00 00 00 00 bc 90 228 | 229 | -> 01 03 d1 00 00 15 bd 39 230 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 231 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 232 | <- 00 00 00 00 00 00 e8 5c 233 | 234 | -> 01 03 23 1c 00 04 8e 4b 235 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 236 | 237 | -> 01 03 d2 00 00 01 bd 72 238 | <- 01 03 02 00 00 b8 44 239 | 240 | -> 01 03 d0 00 00 26 fc d0 241 | <- 01 03 4c 0c fe 0d 01 0d 00 0c fe ee 49 ee 49 ee 49 ee 49 242 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 243 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 244 | <- ee 49 ee 49 ee 49 ee 49 0d 01 0c fe 00 02 00 01 00 03 05 32 245 | <- dd 1f 246 | 247 | -> 01 03 d0 00 00 26 fc d0 248 | <- 01 03 4c 0c fe 0d 01 0d 00 0c fe ee 49 ee 49 ee 49 ee 49 249 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 250 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 251 | <- ee 49 ee 49 ee 49 ee 49 0d 01 0c fe 00 02 00 01 00 03 05 32 252 | <- dd 1f 253 | 254 | -> 01 03 d0 26 00 19 5d 0b 255 | <- 01 03 32 02 44 00 00 00 00 00 00 00 00 00 00 02 58 00 00 256 | <- 00 00 02 4e 02 58 02 44 00 00 00 00 00 64 00 64 30 d3 30 d4 257 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 64 4c 258 | 259 | -> 01 03 d1 15 00 0c 6d 37 260 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 261 | <- 00 00 00 00 00 00 00 00 bc 90 262 | 263 | -> 01 03 d1 00 00 15 bd 39 264 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 265 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 266 | <- 00 00 00 00 00 00 e8 5c 267 | 268 | -> 01 03 23 1c 00 04 8e 4b 269 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 270 | 271 | -> 01 03 d2 00 00 01 bd 72 272 | <- 01 03 02 00 00 b8 44 273 | 274 | -> 01 03 d0 00 00 26 fc d0 275 | <- 01 03 4c 0c fe 0d 01 0d 00 0c fe ee 49 ee 49 ee 49 ee 49 276 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 277 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 278 | <- ee 49 ee 49 ee 49 ee 49 0d 01 0c fe 00 02 00 01 00 03 05 32 279 | <- dd 1f 280 | 281 | -> 01 03 d0 00 00 26 fc d0 282 | <- 01 03 4c 0c fe 0d 01 0d 00 0c fe ee 49 ee 49 ee 49 ee 49 283 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 284 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 285 | <- ee 49 ee 49 ee 49 ee 49 0d 01 0c fe 00 02 00 01 00 03 05 32 286 | <- dd 1f 287 | 288 | -> 01 03 d0 26 00 19 5d 0b 289 | <- 01 03 32 02 44 00 00 00 00 00 00 00 00 00 00 02 58 00 00 290 | <- 00 00 02 4e 02 58 02 44 00 00 00 00 00 64 00 64 30 d3 30 d4 291 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 64 4c 292 | 293 | -> 01 03 d1 15 00 0c 6d 37 294 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 295 | <- 00 00 00 00 00 00 00 00 bc 90 296 | 297 | -> 01 03 d1 00 00 15 bd 39 298 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 299 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 300 | <- 00 00 00 00 00 00 e8 5c 301 | 302 | -> 01 03 23 1c 00 04 8e 4b 303 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 304 | 305 | -> 01 03 d2 00 00 01 bd 72 306 | <- 01 03 02 00 00 b8 44 307 | 308 | -> 01 03 d0 00 00 26 fc d0 309 | <- 01 03 4c 0c fe 0d 01 0d 00 0c fe ee 49 ee 49 ee 49 ee 49 310 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 311 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 312 | <- ee 49 ee 49 ee 49 ee 49 0d 01 0c fe 00 02 00 01 00 03 05 32 313 | <- dd 1f 314 | 315 | -> 01 03 d0 00 00 26 fc d0 316 | <- 01 03 4c 0c fe 0d 01 0d 00 0c fe ee 49 ee 49 ee 49 ee 49 317 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 318 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 319 | <- ee 49 ee 49 ee 49 ee 49 0d 01 0c fe 00 02 00 01 00 03 05 32 320 | <- dd 1f 321 | 322 | -> 01 03 d0 26 00 19 5d 0b 323 | <- 01 03 32 02 44 00 00 00 00 00 00 00 00 00 00 02 58 00 00 324 | <- 00 00 02 4e 02 58 02 44 00 00 00 00 00 64 00 64 30 d3 30 d4 325 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 64 4c 326 | 327 | -> 01 03 d1 15 00 0c 6d 37 328 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 329 | <- 00 00 00 00 00 00 00 00 bc 90 330 | 331 | -> 01 03 d1 00 00 15 bd 39 332 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 333 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 334 | <- 00 00 00 00 00 00 e8 5c 335 | 336 | -> 01 03 23 1c 00 04 8e 4b 337 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 338 | 339 | -> 01 03 d2 00 00 01 bd 72 340 | <- 01 03 02 00 00 b8 44 341 | 342 | -> 01 03 d0 00 00 26 fc d0 343 | <- 01 03 4c 0c fe 0d 01 0d 00 0c fe ee 49 ee 49 ee 49 ee 49 344 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 345 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 346 | <- ee 49 ee 49 ee 49 ee 49 0d 01 0c fe 00 02 00 01 00 03 05 32 347 | <- dd 1f 348 | 349 | -> 01 03 d0 00 00 26 fc d0 350 | <- 01 03 4c 0c fe 0d 01 0d 00 0c fe ee 49 ee 49 ee 49 ee 49 351 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 352 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 353 | <- ee 49 ee 49 ee 49 ee 49 0d 01 0c fe 00 02 00 01 00 03 05 32 354 | <- dd 1f 355 | 356 | -> 01 03 d0 26 00 19 5d 0b 357 | <- 01 03 32 02 44 00 00 00 00 00 00 00 00 00 00 02 58 00 00 358 | <- 00 00 02 4e 02 58 02 44 00 00 00 00 00 64 00 64 30 d3 30 d4 359 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 64 4c 360 | 361 | -> 01 03 d1 15 00 0c 6d 37 362 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 363 | <- 00 00 00 00 00 00 00 00 bc 90 364 | 365 | -> 01 03 d1 00 00 15 bd 39 366 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 367 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 368 | <- 00 00 00 00 00 00 e8 5c 369 | 370 | -> 01 03 23 1c 00 04 8e 4b 371 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 372 | 373 | -> 01 03 d1 00 00 15 bd 39 374 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 375 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 376 | <- 00 00 00 00 00 00 e8 5c 377 | 378 | -> 01 03 23 1c 00 04 8e 4b 379 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 380 | 381 | -> 01 03 d2 00 00 01 bd 72 382 | <- 01 03 02 00 00 b8 44 383 | 384 | -> 01 03 d0 00 00 26 fc d0 385 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 386 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 387 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 388 | <- ee 49 ee 49 ee 49 ee 49 0d 00 0c fd 00 02 00 04 00 03 05 32 389 | <- f3 35 390 | 391 | -> 01 03 d0 00 00 26 fc d0 392 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 393 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 394 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 395 | <- ee 49 ee 49 ee 49 ee 49 0d 00 0c fd 00 02 00 04 00 03 05 32 396 | <- f3 35 397 | 398 | -> 01 03 d0 26 00 19 5d 0b 399 | <- 01 03 32 02 26 00 00 00 00 00 00 00 00 00 00 02 3a 00 00 400 | <- 00 00 02 26 02 3a 02 26 00 00 00 00 00 64 00 64 30 d3 30 d4 401 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 c4 c0 402 | 403 | -> 01 03 d1 15 00 0c 6d 37 404 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 405 | <- 00 00 00 00 00 00 00 00 bc 90 406 | 407 | -> 01 03 d1 00 00 15 bd 39 408 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 409 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 410 | <- 00 00 00 00 00 00 e8 5c 411 | 412 | -> 01 03 23 1c 00 04 8e 4b 413 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 414 | 415 | -> 01 03 d2 00 00 01 bd 72 416 | <- 01 03 02 00 00 b8 44 417 | 418 | -> 01 03 d0 00 00 26 fc d0 419 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 420 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 421 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 422 | <- ee 49 ee 49 ee 49 ee 49 0d 00 0c fd 00 02 00 04 00 03 05 32 423 | <- f3 35 424 | 425 | -> 01 03 d0 00 00 26 fc d0 426 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 427 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 428 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 429 | <- ee 49 ee 49 ee 49 ee 49 0d 00 0c fd 00 02 00 04 00 03 05 32 430 | <- f3 35 431 | 432 | -> 01 03 d0 26 00 19 5d 0b 433 | <- 01 03 32 02 26 00 00 00 00 00 00 00 00 00 00 02 3a 00 00 434 | <- 00 00 02 26 02 3a 02 26 00 00 00 00 00 64 00 64 30 d3 30 d4 435 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 c4 c0 436 | 437 | -> 01 03 d1 15 00 0c 6d 37 438 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 439 | <- 00 00 00 00 00 00 00 00 bc 90 440 | 441 | -> 01 03 d1 00 00 15 bd 39 442 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 443 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 444 | <- 00 00 00 00 00 00 e8 5c 445 | 446 | -> 01 03 23 1c 00 04 8e 4b 447 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 448 | 449 | -> 01 03 d2 00 00 01 bd 72 450 | <- 01 03 02 00 00 b8 44 451 | 452 | -> 01 03 d0 00 00 26 fc d0 453 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 454 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 455 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 456 | <- ee 49 ee 49 ee 49 ee 49 0d 00 0c fd 00 02 00 04 00 03 05 32 457 | <- f3 35 458 | 459 | -> 01 03 d0 00 00 26 fc d0 460 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 461 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 462 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 463 | <- ee 49 ee 49 ee 49 ee 49 0d 00 0c fd 00 02 00 04 00 03 05 32 464 | <- f3 35 465 | 466 | -> 01 03 d0 26 00 19 5d 0b 467 | <- 01 03 32 02 26 00 00 00 00 00 00 00 00 00 00 02 3a 00 00 468 | <- 00 00 02 26 02 3a 02 26 00 00 00 00 00 64 00 64 30 d3 30 d4 469 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 c4 c0 470 | 471 | -> 01 03 d1 15 00 0c 6d 37 472 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 473 | <- 00 00 00 00 00 00 00 00 bc 90 474 | 475 | -> 01 03 d1 00 00 15 bd 39 476 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 477 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 478 | <- 00 00 00 00 00 00 e8 5c 479 | 480 | -> 01 03 23 1c 00 04 8e 4b 481 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 482 | 483 | -> 01 03 d2 00 00 01 bd 72 484 | <- 01 03 02 00 00 b8 44 485 | 486 | -> 01 03 d0 00 00 26 fc d0 487 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 488 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 489 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 490 | <- ee 49 ee 49 ee 49 ee 49 0d 00 0c fd 00 02 00 04 00 03 05 32 491 | <- f3 35 492 | 493 | -> 01 03 d0 00 00 26 fc d0 494 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 495 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 496 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 497 | <- ee 49 ee 49 ee 49 ee 49 0d 00 0c fd 00 02 00 04 00 03 05 32 498 | <- f3 35 499 | 500 | -> 01 03 d0 26 00 19 5d 0b 501 | <- 01 03 32 02 26 00 00 00 00 00 00 00 00 00 00 02 3a 00 00 502 | <- 00 00 02 26 02 3a 02 26 00 00 00 00 00 64 00 64 30 d3 30 d4 503 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 c4 c0 504 | 505 | -> 01 03 d1 15 00 0c 6d 37 506 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 507 | <- 00 00 00 00 00 00 00 00 bc 90 508 | 509 | -> 01 03 d1 00 00 15 bd 39 510 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 511 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 512 | <- 00 00 00 00 00 00 e8 5c 513 | 514 | -> 01 03 23 1c 00 04 8e 4b 515 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 516 | 517 | -> 01 03 d2 00 00 01 bd 72 518 | <- 01 03 02 00 00 b8 44 519 | 520 | -> 01 03 d0 00 00 26 fc d0 521 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 522 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 523 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 524 | <- ee 49 ee 49 ee 49 ee 49 0d 00 0c fd 00 02 00 04 00 03 05 32 525 | <- f3 35 526 | 527 | -> 01 03 d0 00 00 26 fc d0 528 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 529 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 530 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 531 | <- ee 49 ee 49 ee 49 ee 49 0d 00 0c fd 00 02 00 04 00 03 05 32 532 | <- f3 35 533 | 534 | -> 01 03 d0 26 00 19 5d 0b 535 | <- 01 03 32 02 26 00 00 00 00 00 00 00 00 00 00 02 3a 00 00 536 | <- 00 00 02 26 02 3a 02 26 00 00 00 00 00 64 00 64 30 d3 30 d4 537 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 c4 c0 538 | 539 | -> 01 03 d1 15 00 0c 6d 37 540 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 541 | <- 00 00 00 00 00 00 00 00 bc 90 542 | 543 | -> 01 03 d1 00 00 15 bd 39 544 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 545 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 546 | <- 00 00 00 00 00 00 e8 5c 547 | 548 | -> 01 03 23 1c 00 04 8e 4b 549 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 550 | 551 | -> 01 03 d2 00 00 01 bd 72 552 | <- 01 03 02 00 00 b8 44 553 | 554 | -> 01 03 d0 00 00 26 fc d0 555 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 556 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 557 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 558 | <- ee 49 ee 49 ee 49 ee 49 0d 00 0c fd 00 02 00 04 00 03 05 32 559 | <- f3 35 560 | 561 | -> 01 03 d0 00 00 26 fc d0 562 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 563 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 564 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 565 | <- f3 35 566 | 567 | -> 01 03 d0 26 00 19 5d 0b 568 | <- 01 03 32 02 26 00 00 00 00 00 00 00 00 00 00 02 3a 00 00 569 | <- 00 00 02 26 02 3a 02 26 00 00 00 00 00 64 00 64 30 d3 30 d4 570 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 c4 c0 571 | 572 | -> 01 03 d1 15 00 0c 6d 37 573 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 574 | <- 00 00 00 00 00 00 00 00 bc 90 575 | 576 | -> 01 03 d1 00 00 15 bd 39 577 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 578 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 579 | <- 00 00 00 00 00 00 e8 5c 580 | 581 | -> 01 03 23 1c 00 04 8e 4b 582 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 583 | 584 | -> 01 03 d2 00 00 01 bd 72 585 | <- 01 03 02 00 00 b8 44 586 | 587 | -> 01 03 d0 00 00 26 fc d0 588 | <- 01 03 4c 0c fe 0d 01 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 589 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 590 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 591 | <- ee 49 ee 49 ee 49 ee 49 0d 01 0c fd 00 02 00 04 00 04 05 32 592 | <- 14 d9 593 | 594 | -> 01 03 d0 00 00 26 fc d0 595 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 596 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 597 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 598 | <- ee 49 ee 49 ee 49 ee 49 0d 00 0c fd 00 02 00 04 00 03 05 32 599 | <- f3 35 600 | 601 | -> 01 03 d0 26 00 19 5d 0b 602 | <- 01 03 32 02 26 00 00 00 00 00 00 00 00 00 00 02 3a 00 00 603 | <- 00 00 02 26 02 3a 02 26 00 00 00 00 00 64 00 64 30 d3 30 d4 604 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 c4 c0 605 | 606 | -> 01 03 d1 15 00 0c 6d 37 607 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 608 | <- 00 00 00 00 00 00 00 00 bc 90 609 | 610 | -> 01 03 d1 00 00 15 bd 39 611 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 612 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 613 | <- 00 00 00 00 00 00 e8 5c 614 | 615 | -> 01 03 23 1c 00 04 8e 4b 616 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 617 | 618 | -> 01 03 d2 00 00 01 bd 72 619 | <- 01 03 02 00 00 b8 44 620 | 621 | -> 01 03 d0 00 00 26 fc d0 622 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 623 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 624 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 625 | <- ee 49 ee 49 ee 49 ee 49 0d 00 0c fd 00 02 00 04 00 03 05 32 626 | <- f3 35 627 | 628 | -> 01 03 d0 00 00 26 fc d0 629 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 630 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 631 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 632 | <- ee 49 ee 49 ee 49 ee 49 0d 00 0c fd 00 02 00 04 00 03 05 32 633 | <- f3 35 634 | 635 | -> 01 03 d0 26 00 19 5d 0b 636 | <- 01 03 32 02 26 00 00 00 00 00 00 00 00 00 00 02 3a 00 00 637 | <- 00 00 02 26 02 3a 02 26 00 00 00 00 00 64 00 64 30 d3 30 d4 638 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 c4 c0 639 | 640 | -> 01 03 d1 15 00 0c 6d 37 641 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 642 | <- 00 00 00 00 00 00 00 00 bc 90 643 | 644 | -> 01 03 d1 00 00 15 bd 39 645 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 646 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 647 | <- 00 00 00 00 00 00 e8 5c 648 | 649 | -> 01 03 23 1c 00 04 8e 4b 650 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 651 | 652 | -> 01 03 d2 00 00 01 bd 72 653 | <- 01 03 02 00 00 b8 44 654 | 655 | -> 01 03 d0 00 00 26 fc d0 656 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 657 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 658 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 659 | <- ee 49 ee 49 ee 49 ee 49 0d 00 0c fd 00 02 00 04 00 03 05 32 660 | <- f3 35 661 | 662 | -> 01 03 d0 00 00 26 fc d0 663 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 664 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 665 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 666 | <- ee 49 ee 49 ee 49 ee 49 0d 00 0c fd 00 02 00 04 00 03 05 32 667 | <- f3 35 668 | 669 | -> 01 03 d0 26 00 19 5d 0b 670 | <- 01 03 32 02 26 00 00 00 00 00 00 00 00 00 00 02 3a 00 00 671 | <- 00 00 02 26 02 3a 02 26 00 00 00 00 00 64 00 64 30 d3 30 d4 672 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 c4 c0 673 | 674 | -> 01 03 d1 15 00 0c 6d 37 675 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 676 | <- 00 00 00 00 00 00 00 00 bc 90 677 | 678 | -> 01 03 d1 00 00 15 bd 39 679 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 680 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 681 | <- 00 00 00 00 00 00 e8 5c 682 | 683 | -> 01 03 d0 00 00 26 fc d0 684 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 685 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 686 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 687 | <- ee 49 ee 49 ee 49 ee 49 0d 00 0c fd 00 02 00 04 00 03 05 32 688 | <- f3 35 689 | 690 | -> 01 03 d0 26 00 19 5d 0b 691 | 692 | -> 01 03 d1 15 00 0c 6d 37 693 | 694 | -> 01 03 d1 00 00 15 bd 39 695 | <- 01 03 32 02 26 00 00 00 00 00 00 00 00 00 00 02 3a 00 00 696 | <- 00 00 02 26 02 3a 02 26 00 00 00 00 00 64 00 64 30 d3 30 d4 697 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 c4 c0 698 | 699 | -> 01 03 d1 00 00 15 bd 39 700 | 701 | -> 01 03 d2 00 00 01 bd 72 702 | 703 | -> 01 03 d0 00 00 26 fc d0 704 | <- 01 03 02 00 00 b8 44 705 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 706 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 707 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 708 | <- ee 49 ee 49 ee 49 ee 49 0d 00 0c fd 00 02 00 04 00 03 05 32 709 | <- f3 35 710 | 711 | -> 01 03 d0 00 00 26 fc d0 712 | 713 | -> 01 03 d0 00 00 26 fc d0 714 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 715 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 716 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 717 | <- ee 49 ee 49 ee 49 ee 49 0d 00 0c fd 00 02 00 04 00 03 05 32 718 | <- f3 35 719 | 720 | -> 01 03 d1 00 00 15 bd 39 721 | 722 | -> 01 03 23 1c 00 04 8e 4b 723 | 724 | -> 01 03 d1 15 00 0c 6d 37 725 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 726 | 727 | -> 01 03 d0 00 00 26 fc d0 728 | 729 | -> 01 03 23 1c 00 04 8e 4b 730 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 731 | 732 | -> 01 03 23 1c 00 04 8e 4b 733 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 734 | 735 | -> 01 03 d0 00 00 26 fc d0 736 | 737 | -> 01 03 d0 00 00 26 fc d0 738 | 739 | -> 01 03 d0 26 00 19 5d 0b 740 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 741 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 742 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 743 | <- ee 49 ee 49 ee 49 ee 49 0d 00 0c fd 00 02 00 04 00 03 05 32 744 | <- f3 35 745 | 746 | -> 01 03 d1 00 00 15 bd 39 747 | 748 | -> 01 03 d1 00 00 15 bd 39 749 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 750 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 751 | <- 00 00 00 00 00 00 e8 5c 752 | 753 | -> 01 03 d0 00 00 26 fc d0 754 | 755 | -> 01 03 d0 00 00 26 fc d0 756 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 757 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 758 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 759 | <- ee 49 ee 49 ee 49 ee 49 0d 00 0c fd 00 02 00 04 00 03 05 32 760 | <- f3 35 761 | 762 | -> 01 03 23 1c 00 04 8e 4b 763 | 764 | -> 01 03 d2 00 00 01 bd 72 765 | 766 | -> 01 03 d0 26 00 19 5d 0b 767 | <- 01 03 02 00 00 b8 44 768 | 769 | -> 01 03 d0 00 00 26 fc d0 770 | 771 | -> 01 03 d1 15 00 0c 6d 37 772 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 773 | <- 00 00 00 00 00 00 00 00 bc 90 774 | 775 | -> 01 03 d0 00 00 26 fc d0 776 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 777 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 778 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 779 | <- ee 49 ee 49 ee 49 ee 49 0d 00 0c fd 00 02 00 04 00 03 05 32 780 | <- f3 35 781 | 782 | -> 01 03 d2 00 00 01 bd 72 783 | 784 | -> 01 03 d1 15 00 0c 6d 37 785 | <- 01 03 02 00 00 b8 44 786 | 787 | -> 01 03 d0 26 00 19 5d 0b 788 | 789 | -> 01 03 d1 00 00 15 bd 39 790 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 791 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 792 | <- 00 00 00 00 00 00 e8 5c 793 | 794 | -> 01 03 d0 00 00 26 fc d0 795 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 796 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 797 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 798 | <- ee 49 ee 49 ee 49 ee 49 0d 00 0c fd 00 02 00 04 00 03 05 32 799 | <- f3 35 800 | 801 | -> 01 03 d1 15 00 0c 6d 37 802 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 803 | <- 00 00 00 00 00 00 00 00 bc 90 804 | 805 | -> 01 03 d0 00 00 26 fc d0 806 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 807 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 808 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 809 | <- f3 35 810 | 811 | -> 01 03 d1 00 00 15 bd 39 812 | 813 | -> 01 03 d0 00 00 26 fc d0 814 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 815 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 816 | <- 00 00 00 00 00 00 e8 5c 817 | 818 | -> 01 03 d0 26 00 19 5d 0b 819 | 820 | -> 01 03 d1 15 00 0c 6d 37 821 | 822 | -> 01 03 d1 00 00 15 bd 39 823 | 824 | -> 01 03 d1 15 00 0c 6d 37 825 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 826 | <- 00 00 00 00 00 00 00 00 bc 90 827 | 828 | -> 01 03 23 1c 00 04 8e 4b 829 | 830 | -> 01 03 d0 26 00 19 5d 0b 831 | 832 | -> 01 03 d1 15 00 0c 6d 37 833 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 834 | 835 | -> 01 03 d0 00 00 26 fc d0 836 | 837 | -> 01 03 23 1c 00 04 8e 4b 838 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 839 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 840 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 841 | <- ee 49 ee 49 ee 49 ee 49 0d 00 0c fd 00 02 00 04 00 03 05 32 842 | <- f3 35 843 | 844 | -> 01 03 d1 00 00 15 bd 39 845 | 846 | -> 01 03 d0 26 00 19 5d 0b 847 | <- 01 03 32 02 26 00 00 00 00 00 00 00 00 00 00 02 30 00 00 848 | <- 00 00 02 26 02 30 02 26 00 00 00 00 00 64 00 64 30 d3 30 d4 849 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 90 6d 850 | 851 | 852 | 853 | -> 01 03 d0 00 00 26 fc d0 854 | 855 | -> 01 03 23 1c 00 04 8e 4b 856 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 857 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 858 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 859 | <- ee 49 ee 49 ee 49 ee 49 0d 00 0c fd 00 02 00 04 00 03 05 32 860 | <- f3 35 861 | 862 | -> 01 03 d1 15 00 0c 6d 37 863 | 864 | -> 01 03 23 1c 00 04 8e 4b 865 | 866 | -> 01 03 d0 00 00 26 fc d0 867 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 868 | 869 | -> 01 03 d0 00 00 26 fc d0 870 | 871 | -> 01 03 23 1c 00 04 8e 4b 872 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 873 | 874 | -> 01 03 d1 00 00 15 bd 39 875 | 876 | -> 01 03 d0 00 00 26 fc d0 877 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 878 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 879 | <- 00 00 00 00 00 00 e8 5c 880 | 881 | -> 01 03 d0 00 00 26 fc d0 882 | 883 | -> 01 03 d1 00 00 15 bd 39 884 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 885 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 886 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 887 | <- ee 49 ee 49 ee 49 ee 49 0d 00 0c fd 00 02 00 04 00 03 05 32 888 | <- f3 35 889 | 890 | -> 01 03 d1 15 00 0c 6d 37 891 | 892 | -> 01 03 23 1c 00 04 8e 4b 893 | 894 | -> 01 03 d0 00 00 26 fc d0 895 | 896 | -> 01 03 d0 00 00 26 fc d0 897 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 898 | <- 00 00 00 00 00 00 00 00 bc 90 01 83 02 c0 f1 899 | <- 01 03 4c 0c fe 0d 00 0c ff 0c fd ee 49 ee 49 ee 49 ee 49 900 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 901 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 902 | <- ee 49 ee 49 ee 49 ee 49 0d 00 0c fd 00 02 00 04 00 03 05 32 903 | <- f3 35 904 | 905 | -> 01 03 d2 00 00 01 bd 72 906 | 907 | -> 01 03 d1 15 00 0c 6d 37 908 | <- 01 03 02 00 00 b8 44 909 | 910 | -> 01 03 d0 26 00 19 5d 0b 911 | -------------------------------------------------------------------------------- /plugins/Hacien/dev/bms-session.txt: -------------------------------------------------------------------------------- 1 | 2 | -> 01 03 23 1c 00 04 8e 4b 3 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 4 | 5 | -> 01 03 d2 00 00 01 bd 72 6 | <- 01 03 02 00 00 b8 44 7 | 8 | -> 01 03 d0 00 00 26 fc d0 9 | <- 01 03 4c 0d 01 0d 05 0d 03 0d 01 ee 49 ee 49 ee 49 ee 49 10 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 11 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 12 | <- ee 49 ee 49 ee 49 ee 49 0d 05 0d 01 00 02 00 01 00 04 05 33 13 | <- 8a d0 14 | 0D01 = 3329 => Celle 1 15 | 0D05 = 3333 => Celle 2 16 | 0D03 = 3331 => Celle 3 17 | 0D01 = 3329 => Celle 4 18 | ... 19 | 0D05 20 | 0D01 21 | 0533 = 1331 => Spenning 22 | 23 | -> 01 03 d0 26 00 19 5d 0b 24 | <- 01 03 32 02 58 00 00 00 00 00 00 00 00 00 00 02 6c 00 00 18 25 | <- 00 00 02 6c 02 6c 02 58 00 00 00 00 00 64 00 64 30 d3 30 d4 26 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 a1 c2 27 | 28 | 02 = 2 29 | 58 = 88 30 | 0258 = 600 31 | 6C = 108 32 | 026C = 620 33 | 6C = 108 34 | 026C = 620 35 | 6C = 108 36 | 026C = 620 37 | 58 = 88 38 | 0258 = 600 39 | 64 = 100 (SOC?) 40 | 30D3 = 12499 => Ah 41 | 30D4 = 12500 => Max Ah 42 | 30D4 = 12500 => Max Ah again? 43 | 06 = 6 -> Charge cycles? 44 | 45 | 46 | -> 01 03 d1 15 00 0c 6d 37 47 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 48 | <- 00 00 00 00 00 00 00 00 bc 90 49 | 50 | -> 01 03 d1 00 00 15 bd 39 51 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 52 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 53 | <- 00 00 00 00 00 00 e8 5c 54 | 55 | -> 01 03 23 1c 00 04 8e 4b 56 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 57 | 58 | -> 01 03 d2 00 00 01 bd 72 59 | <- 01 03 02 00 00 b8 44 60 | 61 | -> 01 03 d0 00 00 26 fc d0 62 | <- 01 03 4c 0d 01 0d 05 0d 03 0d 02 ee 49 ee 49 ee 49 ee 49 63 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 64 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 65 | <- ee 49 ee 49 ee 49 ee 49 0d 05 0d 01 00 02 00 01 00 04 05 33 66 | <- 9b e3 67 | 68 | 0D01 = 3329 => Celle 1 69 | 0D05 = 3333 => Celle 2 70 | 0D03 = 3331 => Celle 3 71 | 0D02 = 3330 => Celle 4 72 | ... 73 | 0D05 74 | 0D01 75 | 0533 = 1331 => Spenning 76 | 77 | -> 01 03 d0 26 00 19 5d 0b 78 | <- 01 03 32 02 58 00 00 00 00 00 00 00 00 00 00 02 62 00 00 79 | <- 00 00 02 6c 02 62 02 58 00 00 00 00 00 64 00 64 30 d3 30 d4 80 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 83 a8 81 | 82 | 02 = 2 83 | 58 = 88 84 | 0258 = 600 85 | 62 = 98 86 | 0262 = 610 87 | 6C = 108 88 | 026C = 620 89 | 62 = 98 90 | 0262 = 610 91 | 58 = 88 92 | 0258 = 600 93 | 64 = 100 (SOC?) 94 | 30D3 = 12499 => Ah 95 | 30D4 = 12500 => Max Ah 96 | 30D4 = 12500 => Max Ah again? 97 | 06 = 6 -> Charge cycles? 98 | 99 | 100 | -> 01 03 d0 00 00 26 fc d0 101 | <- 01 03 4c 0d 01 0d 05 0d 02 0d 01 ee 49 ee 49 ee 49 ee 49 102 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 103 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 104 | <- ee 49 ee 49 ee 49 ee 49 0d 05 0d 01 00 02 00 01 00 04 05 33 105 | <- 4e ec 106 | 107 | 0D01 = 3329 => Celle 1 108 | 0D05 = 3333 => Celle 2 109 | 0D02 = 3330 => Celle 3 110 | 0D01 = 3329 => Celle 4 111 | ... 112 | 0D05 113 | 0D01 114 | 0533 = 1331 => Spenning 115 | 116 | -> 01 03 d0 26 00 19 5d 0b 117 | <- 01 03 32 02 58 00 00 00 00 00 00 00 00 00 00 02 6c 00 00 118 | <- 00 00 02 6c 02 6c 02 58 00 00 00 03 00 64 00 64 30 d3 30 d4 119 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 f5 27 120 | 121 | 02 = 2 122 | 58 = 88 123 | 0258 = 600 124 | 62 = 98 125 | 0262 = 610 126 | 6C = 108 127 | 026C = 620 128 | 62 = 98 129 | 0262 = 610 130 | 58 = 88 131 | 0258 = 600 132 | 64 = 100 (SOC?) 133 | 30D3 = 12499 => Ah 134 | 30D4 = 12500 => Max Ah 135 | 30D4 = 12500 => Max Ah again? 136 | 06 = 6 -> Charge cycles? 137 | 138 | -> 01 03 d0 26 00 19 5d 0b 139 | <- 01 03 32 02 58 00 00 00 00 00 00 00 00 00 00 02 6c 00 00 140 | <- 00 00 02 6c 02 6c 02 58 00 00 00 02 00 64 00 64 30 d3 30 d4 141 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 38 bb 142 | 143 | 02 som var 03 = forbruk? 144 | 0.3 A 145 | 146 | -> 01 03 d1 15 00 0c 6d 37 147 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 148 | <- 00 00 00 00 00 00 00 00 bc 90 149 | 150 | 00F8 (HEX)= 248 (DEC). Output value 0~1650 corresponds to -40~125℃. Temperature=125-(-40)*248/1650-40=-15.2℃ 151 | 152 | -45 + 153 | 154 | = 18,99 = -45 + (0x5D9A * 175) / 0xFFFF (° C) 155 | 156 | 157 | 158 | -> 01 03 d1 00 00 15 bd 39 159 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 160 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 161 | <- 00 00 00 00 00 00 e8 5c 162 | 163 | -> 01 03 23 1c 00 04 8e 4b 164 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 165 | 166 | -> 01 03 d2 00 00 01 bd 72 167 | <- 01 03 02 00 00 b8 44 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | -> 01 03 d0 00 00 26 fc d0 176 | <- 01 03 4c 0d 01 0d 05 0d 03 0d 01 ee 49 ee 49 ee 49 ee 49 177 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 178 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 179 | <- ee 49 ee 49 ee 49 ee 49 0d 05 0d 01 00 02 00 01 00 04 05 33 180 | <- 8a d0 181 | 182 | -> 01 03 d0 00 00 26 fc d0 183 | <- 01 03 4c 0d 01 0d 05 0d 03 0d 01 ee 49 ee 49 ee 49 ee 49 184 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 185 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 186 | <- ee 49 ee 49 ee 49 ee 49 0d 05 0d 01 00 02 00 01 00 04 05 33 187 | <- 8a d0 188 | 189 | -> 01 03 d0 26 00 19 5d 0b 190 | <- 01 03 32 02 58 00 00 00 00 00 00 00 00 00 00 02 6c 00 00 191 | <- 00 00 02 6c 02 6c 02 58 00 00 00 00 00 64 00 64 30 d3 30 d4 192 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 a1 c2 193 | 194 | -> 01 03 d1 15 00 0c 6d 37 195 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 196 | <- 00 00 00 00 00 00 00 00 bc 90 197 | 198 | -> 01 03 d1 00 00 15 bd 39 199 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 200 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 201 | <- 00 00 00 00 00 00 e8 5c 202 | 203 | -> 01 03 23 1c 00 04 8e 4b 204 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 205 | 206 | -> 01 03 d2 00 00 01 bd 72 207 | <- 01 03 02 00 00 b8 44 208 | 209 | -> 01 03 d0 00 00 26 fc d0 210 | <- 01 03 4c 0d 01 0d 05 0d 03 0d 01 ee 49 ee 49 ee 49 ee 49 211 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 212 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 213 | <- ee 49 ee 49 ee 49 ee 49 0d 05 0d 01 00 02 00 01 00 04 05 33 214 | <- 8a d0 215 | 216 | -> 01 03 d0 00 00 26 fc d0 217 | <- 01 03 4c 0d 01 0d 05 0d 03 0d 01 ee 49 ee 49 ee 49 ee 49 218 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 219 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 220 | <- ee 49 ee 49 ee 49 ee 49 0d 05 0d 01 00 02 00 01 00 04 05 33 221 | <- 8a d0 222 | 223 | -> 01 03 d0 26 00 19 5d 0b 224 | <- 01 03 32 02 58 00 00 00 00 00 00 00 00 00 00 02 6c 00 00 225 | <- 00 00 02 6c 02 6c 02 58 00 00 00 00 00 64 00 64 30 d3 30 d4 226 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 a1 c2 227 | 228 | -> 01 03 d1 15 00 0c 6d 37 229 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 230 | <- 00 00 00 00 00 00 00 00 bc 90 231 | 232 | -> 01 03 d1 00 00 15 bd 39 233 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 234 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 235 | <- 00 00 00 00 00 00 e8 5c 236 | 237 | -> 01 03 23 1c 00 04 8e 4b 238 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 239 | 240 | -> 01 03 d2 00 00 01 bd 72 241 | <- 01 03 02 00 00 b8 44 242 | 243 | -> 01 03 d0 00 00 26 fc d0 244 | <- 01 03 4c 0d 01 0d 05 0d 03 0d 01 ee 49 ee 49 ee 49 ee 49 245 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 246 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 247 | <- ee 49 ee 49 ee 49 ee 49 0d 05 0d 01 00 02 00 01 00 04 05 33 248 | <- 8a d0 249 | 250 | -> 01 03 d0 00 00 26 fc d0 251 | <- 01 03 4c 0d 01 0d 05 0d 03 0d 01 ee 49 ee 49 ee 49 ee 49 252 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 253 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 254 | <- ee 49 ee 49 ee 49 ee 49 0d 05 0d 01 00 02 00 01 00 04 05 33 255 | <- 8a d0 256 | 257 | -> 01 03 d0 26 00 19 5d 0b 258 | <- 01 03 32 02 58 00 00 00 00 00 00 00 00 00 00 02 6c 00 00 259 | <- 00 00 02 6c 02 6c 02 58 00 00 00 00 00 64 00 64 30 d3 30 d4 260 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 a1 c2 261 | 262 | -> 01 03 d1 15 00 0c 6d 37 263 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 264 | <- 00 00 00 00 00 00 00 00 bc 90 265 | 266 | -> 01 03 d1 00 00 15 bd 39 267 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 268 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 269 | <- 00 00 00 00 00 00 e8 5c 270 | 271 | -> 01 03 23 1c 00 04 8e 4b 272 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 273 | 274 | -> 01 03 d2 00 00 01 bd 72 275 | <- 01 03 02 00 00 b8 44 276 | 277 | -> 01 03 d0 00 00 26 fc d0 278 | <- 01 03 4c 0d 01 0d 05 0d 03 0d 01 ee 49 ee 49 ee 49 ee 49 279 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 280 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 281 | <- ee 49 ee 49 ee 49 ee 49 0d 05 0d 01 00 02 00 01 00 04 05 33 282 | <- 8a d0 283 | 284 | -> 01 03 d0 00 00 26 fc d0 285 | <- 01 03 4c 0d 01 0d 05 0d 03 0d 01 ee 49 ee 49 ee 49 ee 49 286 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 287 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 288 | <- ee 49 ee 49 ee 49 ee 49 0d 05 0d 01 00 02 00 01 00 04 05 33 289 | <- 8a d0 290 | 291 | -> 01 03 d0 26 00 19 5d 0b 292 | <- 01 03 32 02 58 00 00 00 00 00 00 00 00 00 00 02 6c 00 00 293 | <- 00 00 02 6c 02 6c 02 58 00 00 00 00 00 64 00 64 30 d3 30 d4 294 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 a1 c2 295 | 296 | -> 01 03 d1 15 00 0c 6d 37 297 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 298 | <- 00 00 00 00 00 00 00 00 bc 90 299 | 300 | -> 01 03 d1 00 00 15 bd 39 301 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 302 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 303 | <- 00 00 00 00 00 00 e8 5c 304 | 305 | -> 01 03 23 1c 00 04 8e 4b 306 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 307 | 308 | -> 01 03 d2 00 00 01 bd 72 309 | <- 01 03 02 00 00 b8 44 310 | 311 | -> 01 03 d0 00 00 26 fc d0 312 | <- 01 03 4c 0d 01 0d 05 0d 03 0d 01 ee 49 ee 49 ee 49 ee 49 313 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 314 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 315 | <- ee 49 ee 49 ee 49 ee 49 0d 05 0d 01 00 02 00 01 00 04 05 33 316 | <- 8a d0 317 | 318 | -> 01 03 d0 00 00 26 fc d0 319 | <- 01 03 4c 0d 01 0d 05 0d 03 0d 01 ee 49 ee 49 ee 49 ee 49 320 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 321 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 322 | <- ee 49 ee 49 ee 49 ee 49 0d 05 0d 01 00 02 00 01 00 04 05 33 323 | <- 8a d0 324 | 325 | -> 01 03 d0 26 00 19 5d 0b 326 | <- 01 03 32 02 58 00 00 00 00 00 00 00 00 00 00 02 6c 00 00 327 | <- 00 00 02 6c 02 6c 02 58 00 00 00 00 00 64 00 64 30 d3 30 d4 328 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 a1 c2 329 | 330 | -> 01 03 d1 15 00 0c 6d 37 331 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 332 | <- 00 00 00 00 00 00 00 00 bc 90 333 | 334 | -> 01 03 d1 00 00 15 bd 39 335 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 336 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 337 | <- 00 00 00 00 00 00 e8 5c 338 | 339 | -> 01 03 23 1c 00 04 8e 4b 340 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 341 | 342 | -> 01 03 d2 00 00 01 bd 72 343 | <- 01 03 02 00 00 b8 44 344 | 345 | -> 01 03 d0 00 00 26 fc d0 346 | <- 01 03 4c 0d 01 0d 05 0d 03 0d 01 ee 49 ee 49 ee 49 ee 49 347 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 348 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 349 | <- ee 49 ee 49 ee 49 ee 49 0d 05 0d 01 00 02 00 01 00 04 05 33 350 | <- 8a d0 351 | 352 | -> 01 03 d0 00 00 26 fc d0 353 | <- 01 03 4c 0d 01 0d 05 0d 03 0d 01 ee 49 ee 49 ee 49 ee 49 354 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 355 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 356 | <- ee 49 ee 49 ee 49 ee 49 0d 05 0d 01 00 02 00 01 00 04 05 33 357 | <- 8a d0 358 | 359 | -> 01 03 d0 26 00 19 5d 0b 360 | <- 01 03 32 02 58 00 00 00 00 00 00 00 00 00 00 02 6c 00 00 361 | <- 00 00 02 6c 02 6c 02 58 00 00 00 00 00 64 00 64 30 d3 30 d4 362 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 a1 c2 363 | 364 | -> 01 03 d1 15 00 0c 6d 37 365 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 366 | <- 00 00 00 00 00 00 00 00 bc 90 367 | 368 | -> 01 03 d1 00 00 15 bd 39 369 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 370 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 371 | <- 00 00 00 00 00 00 e8 5c 372 | 373 | -> 01 03 23 1c 00 04 8e 4b 374 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 375 | 376 | -> 01 03 d2 00 00 01 bd 72 377 | <- 01 03 02 00 00 b8 44 378 | 379 | -> 01 03 d0 00 00 26 fc d0 380 | <- 01 03 4c 0d 01 0d 05 0d 03 0d 01 ee 49 ee 49 ee 49 ee 49 381 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 382 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 383 | <- ee 49 ee 49 ee 49 ee 49 0d 05 0d 01 00 02 00 01 00 04 05 33 384 | <- 8a d0 385 | 386 | -> 01 03 d0 00 00 26 fc d0 387 | <- 01 03 4c 0d 01 0d 05 0d 03 0d 01 ee 49 ee 49 ee 49 ee 49 388 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 389 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 390 | <- ee 49 ee 49 ee 49 ee 49 0d 05 0d 01 00 02 00 01 00 04 05 33 391 | <- 8a d0 392 | 393 | -> 01 03 d0 26 00 19 5d 0b 394 | <- 01 03 32 02 58 00 00 00 00 00 00 00 00 00 00 02 6c 00 00 395 | <- 00 00 02 6c 02 6c 02 58 00 00 00 00 00 64 00 64 30 d3 30 d4 396 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 a1 c2 397 | 398 | -> 01 03 d1 15 00 0c 6d 37 399 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 400 | <- 00 00 00 00 00 00 00 00 bc 90 401 | 402 | -> 01 03 d1 00 00 15 bd 39 403 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 404 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 405 | <- 00 00 00 00 00 00 e8 5c 406 | 407 | -> 01 03 23 1c 00 04 8e 4b 408 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 409 | 410 | -> 01 03 d2 00 00 01 bd 72 411 | <- 01 03 02 00 00 b8 44 412 | 413 | -> 01 03 d0 00 00 26 fc d0 414 | <- 01 03 4c 0d 01 0d 05 0d 03 0d 01 ee 49 ee 49 ee 49 ee 49 415 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 416 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 417 | <- ee 49 ee 49 ee 49 ee 49 0d 05 0d 01 00 02 00 01 00 04 05 33 418 | <- 8a d0 419 | 420 | -> 01 03 d0 00 00 26 fc d0 421 | <- 01 03 4c 0d 01 0d 05 0d 03 0d 01 ee 49 ee 49 ee 49 ee 49 422 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 423 | <- ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 ee 49 424 | <- ee 49 ee 49 ee 49 ee 49 0d 05 0d 01 00 02 00 01 00 04 05 33 425 | <- 8a d0 426 | 427 | -> 01 03 d0 26 00 19 5d 0b 428 | <- 01 03 32 02 58 00 00 00 00 00 00 00 00 00 00 02 6c 00 00 429 | <- 00 00 02 6c 02 6c 02 58 00 00 00 00 00 64 00 64 30 d3 30 d4 430 | <- 30 d4 00 06 00 00 00 00 00 00 00 00 00 00 a1 c2 431 | 432 | -> 01 03 d1 15 00 0c 6d 37 433 | <- 01 03 18 24 0c 00 00 02 a7 00 00 00 00 00 00 00 00 00 00 434 | <- 00 00 00 00 00 00 00 00 bc 90 435 | 436 | -> 01 03 d1 00 00 15 bd 39 437 | <- 01 03 2a 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 438 | <- 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 439 | <- 00 00 00 00 00 00 e8 5c 440 | 441 | -> 01 03 23 1c 00 04 8e 4b 442 | <- 01 03 08 00 04 00 01 00 06 00 0a 8d d1 443 | 444 | -> 01 03 d2 00 00 01 bd 72 445 | -------------------------------------------------------------------------------- /plugins/Hacien/dev/bms.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Olen/solar-monitor/d2f84377e027af41e7e37e82047f81882165fa74/plugins/Hacien/dev/bms.pcapng -------------------------------------------------------------------------------- /plugins/Hacien/dev/parse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import json 4 | import pprint 5 | import re 6 | 7 | 8 | def validCrc(msg: list) -> bool: 9 | return len(msg) > 0 and modbusCrc(msg) == 0 10 | 11 | def modbusCrc(msg: list) -> int: 12 | crc = 0xFFFF 13 | for n in msg: 14 | crc ^= n 15 | for i in range(8): 16 | if crc & 1: 17 | crc >>= 1 18 | crc ^= 0xA001 19 | else: 20 | crc >>= 1 21 | return crc 22 | 23 | # msg = [0x01, 0x03, 0xd0, 0x26, 0x00, 0x19, 0x5d, 0x0b] 24 | # print(validCrc(msg)) 25 | # msg = [0x01, 0x03, 0xd0, 0x26, 0x00, 0x19] 26 | # print(validCrc(msg)) 27 | # crc = modbusCrc(msg) 28 | # print("0x%04X"%(crc)) 29 | # 30 | # ba = crc.to_bytes(2, byteorder='little') 31 | # print("%02X %02X"%(ba[0], ba[1])) 32 | # 33 | # sys.exit() 34 | # 35 | f = open("bms-raw-2024-03-20.json") 36 | data = json.load(f) 37 | 38 | # pprint.pprint(data) 39 | buffer = [] 40 | for packet in data: 41 | if 'btatt' in packet['_source']['layers']: 42 | # print(packet['_source']['layers']['frame']['frame.time']) 43 | if 'btgatt.nordic.uart_tx_raw' in packet['_source']['layers']['btatt']: 44 | buffer = [] 45 | tx_string = packet['_source']['layers']['btatt']['btgatt.nordic.uart_tx_raw'][0] 46 | tx_splitted = re.findall('.{1,2}', tx_string) 47 | print("") 48 | print("->", " ".join(tx_splitted)) 49 | # print(f"-> {packet['_source']['layers']['btatt']['btgatt.nordic.uart_tx_raw'][0]}") 50 | # print(f" len: {len(packet['_source']['layers']['btatt']['btgatt.nordic.uart_tx'])}") 51 | # print(f" bytes: {bytes(packet['_source']['layers']['btatt']['btgatt.nordic.uart_tx'], 'utf-8')}") 52 | if 'btgatt.nordic.uart_rx_raw' in packet['_source']['layers']['btatt']: 53 | string = packet['_source']['layers']['btatt']['btgatt.nordic.uart_rx_raw'][0] 54 | splitted = re.findall('.{1,2}', string) 55 | buffer = buffer + [int(i, 16) for i in splitted] 56 | # buffer = buffer + splitted 57 | if validCrc(buffer): 58 | checksum = buffer[-2:] 59 | if len(buffer) > 10 and buffer[2] == 0x4c: 60 | print(f"-> {tx_splitted}") 61 | cell1 = buffer[3]*256 + buffer[4] 62 | cell2 = buffer[5]*256 + buffer[6] 63 | cell3 = buffer[7]*256 + buffer[8] 64 | cell4 = buffer[9]*256 + buffer[10] 65 | c_voltage = buffer[-4]*256 + buffer[-3] 66 | voltage = c_voltage / 100 67 | print(cell1, cell2, cell3, cell4, voltage) 68 | elif len(buffer) > 10 and buffer[2] == 0x32: 69 | print(f"-> {tx_splitted}") 70 | use = (buffer[29]*256 + buffer[30]) / 10 71 | soc1 = buffer[32] 72 | soc2 = buffer[34] 73 | current_ah = buffer[35]*256 + buffer[36] 74 | total_ah1 = buffer[37]*256 + buffer[38] 75 | total_ah2 = buffer[39]*256 + buffer[40] 76 | cycles = buffer[42] 77 | print(use, soc1, soc2, current_ah, total_ah1, total_ah2, cycles) 78 | 79 | temp1 = buffer[3]*256+buffer[4] 80 | print("T1", temp1) 81 | temp2 = temp1 - 380 82 | print("T2", temp2) 83 | temp3 = temp2 / 10 84 | print("T3", temp3) 85 | 86 | # 0258 = 600 22 87 | # 0244 = 580 20 - 20 88 | # 0226 = 550 17 - 30 89 | # 90 | # 017C = 380 0 - 170 91 | # 0000 = 0 -38 92 | buffer = [] 93 | 94 | # elif len(buffer) > 10 and buffer[2] == 0x18: 95 | # pass 96 | # # temp1 = buffer[7]*256 + buffer[8] 97 | # # print("T1", temp1) 98 | # # temp2 = -50 + (temp1*175)/1650 99 | # # # -50+(679*175)/1650 100 | 101 | # # print("T2", temp2) 102 | # # temp3 = (100-(-30))*temp1/1650-30 103 | # # print("T3", temp3) 104 | # # # print("T", temp) 105 | # # # 02A7 106 | # # # (100-(-30))*679/1650-30 107 | 108 | 109 | # else: 110 | # pass 111 | # # print(buffer) 112 | 113 | # print("<-", " ".join(splitted)) 114 | # print(f"<- {packet['_source']['layers']['btatt']['btgatt.nordic.uart_rx_raw'][0]}") 115 | # print(f" len: {len(packet['_source']['layers']['btatt']['btgatt.nordic.uart_rx'])}") 116 | # print(f" bytes: {bytes(packet['_source']['layers']['btatt']['btgatt.nordic.uart_rx'], 'utf-8')}") 117 | # pprint.pprint(packet['_source']['layers']['btatt']) 118 | -------------------------------------------------------------------------------- /plugins/Hacien/dev/read.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import json 4 | import pprint 5 | import re 6 | 7 | 8 | f = open("bms-raw-2024-03-20.json") 9 | data = json.load(f) 10 | 11 | # pprint.pprint(data) 12 | for packet in data: 13 | if 'btatt' in packet['_source']['layers']: 14 | # print(packet['_source']['layers']['frame']['frame.time']) 15 | if 'btgatt.nordic.uart_tx_raw' in packet['_source']['layers']['btatt']: 16 | string = packet['_source']['layers']['btatt']['btgatt.nordic.uart_tx_raw'][0] 17 | splitted = re.findall('.{1,2}', string) 18 | print("") 19 | print("->", " ".join(splitted)) 20 | # print(f"-> {packet['_source']['layers']['btatt']['btgatt.nordic.uart_tx_raw'][0]}") 21 | # print(f" len: {len(packet['_source']['layers']['btatt']['btgatt.nordic.uart_tx'])}") 22 | # print(f" bytes: {bytes(packet['_source']['layers']['btatt']['btgatt.nordic.uart_tx'], 'utf-8')}") 23 | if 'btgatt.nordic.uart_rx_raw' in packet['_source']['layers']['btatt']: 24 | string = packet['_source']['layers']['btatt']['btgatt.nordic.uart_rx_raw'][0] 25 | splitted = re.findall('.{1,2}', string) 26 | print("<-", " ".join(splitted)) 27 | # print(f"<- {packet['_source']['layers']['btatt']['btgatt.nordic.uart_rx_raw'][0]}") 28 | # print(f" len: {len(packet['_source']['layers']['btatt']['btgatt.nordic.uart_rx'])}") 29 | # print(f" bytes: {bytes(packet['_source']['layers']['btatt']['btgatt.nordic.uart_rx'], 'utf-8')}") 30 | # pprint.pprint(packet['_source']['layers']['btatt']) 31 | -------------------------------------------------------------------------------- /plugins/Meritsun/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # from __future__ import print_function 4 | import os 5 | import sys 6 | import time 7 | from datetime import datetime 8 | 9 | # import duallog 10 | import logging 11 | 12 | # duallog.setup('SmartPower', minLevel=logging.INFO) 13 | 14 | 15 | class Config(): 16 | SEND_ACK = False 17 | NEED_POLLING = False 18 | NOTIFY_SERVICE_UUID = "0000ffe0-0000-1000-8000-00805f9b34fb" 19 | NOTIFY_CHAR_UUID = "0000ffe4-0000-1000-8000-00805f9b34fb" 20 | 21 | class Util(): 22 | ''' 23 | Class for reading and parsing data from various SmartPower-BLE-streams 24 | 25 | These devices encode the data in a really crazy way. 26 | Data is streamed continously, and you need to find certain "start of data" and "end of data" 27 | markers to get the correct values. 28 | The data is then divided into chuks of up to 122 bytes 29 | 30 | Example chunk: [56, 49, 51, 54, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 65, 48, 57, 65, 48, 49, 48, 48, 51, 53, 48, 48, 54, 52, 48, 48, 67, 56, 48, 65, 56, 48, 56, 56, 48, 55, 66, 54, 56, 50, 48, 69, 54, 50, 48, 68, 55, 53, 48, 68, 50, 56, 48, 68, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 54, 68, 56, 12, 12, 12, 12, 12, 12, 12, 12] 31 | 32 | Data is read as "little endian" and is ascii-encoded hex characters 33 | In the above example, the voltage is encoded in the first 8 bytes as follows: 34 | Read bytes 7 and 8 (48, 48) 35 | Encode these as ascii-characters "0, 0" (String: "00" 36 | Read bytes 5 and 6 (48, 48) 37 | Encode these as ascii-characters "0, 0" (Append to string: "0000") 38 | Read bytes 3 and 4 (51, 54) 39 | Encode these as ascii-characters "3, 6" (Append to string: "000036") 40 | Read bytes 1 and 2 (56, 49) 41 | Encode these as ascii-characters "8, 1" (Append to string: "00003681") 42 | 43 | convert this hex-string to decimal: 0x00003681 = 13953 = 13.953 V 44 | ''' 45 | 46 | 47 | def __init__(self, power_device): 48 | self.SOI = 1 49 | self.INFO = 2 50 | self.EOI = 3 51 | self.START_VAL = 146 52 | self.END_VAL = 12 53 | self.RecvDataType = self.SOI 54 | self.RevBuf = [None] * 122 55 | self.Revindex = 0 56 | # self.TAG = "SmartPowerUtil" 57 | self.PowerDevice = power_device 58 | self.end = 0 59 | self.prev_values = [] 60 | 61 | def getValue(self, buf, start, end): 62 | try: 63 | # bytes = buf[0:8] 64 | chars = list(map(chr, buf[start:end + 1])) 65 | values = [ ''.join(x) for x in zip(chars[0::2], chars[1::2]) ] 66 | return int("".join(reversed(values)), 16) 67 | except Exception as e: 68 | return 0 69 | 70 | 71 | 72 | 73 | def getValue_old(self, buf, start, end): 74 | # Reads "start" -> "end" from "buf" and return the hex-characters in the correct order 75 | e = end + 1 76 | b = end - 1 77 | string = "" 78 | while b >= start: 79 | chrs = buf[b:e] 80 | # logging.debug(chrs) 81 | e = b 82 | b = b - 2 83 | string += chr(chrs[0]) + chr(chrs[1]) 84 | # logging.debug(string) 85 | try: 86 | ret = int(string, 16) 87 | except Exception as e: 88 | ret = 0 89 | return ret 90 | 91 | 92 | def asciitochar(self, a, b): 93 | x1 = 0 94 | if a >= 48 and a <= 57: 95 | x1 = a - 48 96 | elif a < 65 or a > 70: 97 | x1 = 0 98 | else: 99 | x1 = (a - 65) + 10 100 | x2 = x1 << 4 101 | if b >= 48 and b <= 57: 102 | return x2 + (b - 48) 103 | if b < 65 or b > 70: 104 | return x2 + 0 105 | return x2 + (b - 65) + 10 106 | 107 | 108 | def validateChecksum(self, buf): 109 | # logging.debug(f"Checksum-calc: buf: {buf}") 110 | Chksum1 = 0 111 | Chksum2 = 0 112 | j = 1 113 | while j < self.end - 5: 114 | Chksum1 = self.getValue(buf, j, j + 1) + Chksum1 115 | # logging.debug(f"Checksum1-calc: j: {j} byteval: {self.getValue(buf, j, j + 1)}, Checksum1: {Chksum1}") 116 | j += 2 117 | # logging.debug("Checksum 1: {}".format(Chksum1)) 118 | 119 | Chksum2 = (self.getValue(buf, j, j + 1) << 8) + self.getValue(buf, j + 2, j + 3) 120 | # logging.debug(f"Checksum2-calc: j: {j} byteval: {self.getValue(buf, j, j + 1)}, Shifted: {self.getValue(buf, j, j + 1) << 8}, Byteval2: {self.getValue(buf, j + 2, j + 3)}") 121 | 122 | # logging.debug("Checksum 2: {}".format(Chksum2)) 123 | # logging.info("C1 {} C2 {}".format(Chksum1, Chksum2)) 124 | if Chksum1 == Chksum2: 125 | return True 126 | return False 127 | 128 | 129 | def notificationUpdate(self, data, char): 130 | # Gets the binary data from the BLE-device and converts it to a list of hex-values 131 | logging.debug("broadcastUpdate Start {} {}".format(data, data.hex())) 132 | if self.PowerDevice.config.getboolean('monitor', 'debug', fallback=False): 133 | with open(f"/tmp/{self.PowerDevice.alias()}.log", 'a') as debugfile: 134 | debugfile.write(f"{datetime.now()} <- {data.hex()}\n") 135 | 136 | # logging.debug("broadcastUpdate Start {} {}".format(data, self.RevBuf)) 137 | # logging.debug("RevIndex {}".format(self.Revindex)) 138 | # logging.debug("SOI {}".format(self.SOI)) 139 | # logging.debug("RecvDataType start {}".format(self.Revindex)) 140 | cmdData = "" 141 | if data != None and len(data): 142 | i = 0 143 | while i < len(data): 144 | # logging.debug("Revindex {} {} Data: {}".format(i, self.Revindex, data[i])) 145 | # logging.debug("RevBuf begin {}".format(self.RevBuf)) 146 | if self.Revindex > 121: 147 | # We have read more than 121 bytes, and don't care about the rest 148 | # logging.debug("Revindex > 121 - parsing done") 149 | self.Revindex = 0 150 | self.end = 0 151 | self.RecvDataType = self.SOI 152 | 153 | if self.RecvDataType == self.SOI: 154 | # 1. We start here, reading byte by byte until we get to the number 146 (hex 92) 155 | # Example: 30 30 30 35 39 35 0c 0c 0c 0c 0c 0c 0c 0c 92 37 37 156 | # ^^ 157 | 158 | if data[i] == self.START_VAL: 159 | # When we find 146, we start filling the message buffer, and set a flag to read more data: 160 | self.RecvDataType = self.INFO 161 | self.RevBuf[self.Revindex] = data[i] 162 | self.Revindex = self.Revindex + 1 163 | elif self.RecvDataType == self.INFO: 164 | # 2. The INFO-flag i set, lets continue to fill the buffer 165 | 166 | self.RevBuf[self.Revindex] = data[i] 167 | self.Revindex = self.Revindex + 1 168 | 169 | # The number 12 (hex 0C) marks the end of the message 170 | if data[i] == self.END_VAL: 171 | if self.end < 110: 172 | # Not sure why we need this... 173 | self.end = self.Revindex 174 | if self.Revindex == 121: 175 | # We have read 121 bytes and that marks the end of the buffer 176 | self.RecvDataType = self.EOI 177 | elif self.RecvDataType == self.EOI: 178 | # 3. We should now have a buffer with 121 bytes, 179 | # starting with the first byte after value 146 (hex 92) 180 | 181 | # logging.debug("RecvDataType == 3 -> EOI") 182 | # logging.debug("Validate Checksum: {}".format(self.validateChecksum(self.RevBuf))) 183 | if self.validateChecksum(self.RevBuf): 184 | cmdData = self.RevBuf[1:self.Revindex] 185 | self.Revindex = 0 186 | self.end = 0 187 | self.RecvDataType = self.SOI 188 | return self.handleMessage(cmdData) 189 | self.Revindex = 0 190 | self.end = 0 191 | self.RecvDataType = self.SOI 192 | i += 1 193 | # logging.debug("broadcastUpdate End cmdData: {} RevBuf {}".format(cmdData, self.RevBuf)) 194 | return False 195 | 196 | 197 | 198 | 199 | 200 | 201 | def handleMessage(self, message): 202 | # Accepts a list of hex-characters, and returns the human readable values into the powerDevice object 203 | # 2024-03-22 10:22:51,195 DEBUG : handleMessage [56, 49, 51, 54, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 65, 48, 57, 65, 48, 49, 48, 48, 51, 53, 48, 48, 54, 52, 48, 48, 67, 56, 48, 65, 56, 48, 56, 56, 48, 55, 66, 54, 56, 50, 48, 69, 54, 50, 48, 68, 55, 53, 48, 68, 50, 56, 48, 68, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 54, 68, 56, 12, 12, 12, 12, 12, 12, 12, 12] 204 | 205 | logging.debug("handleMessage {}".format(message)) 206 | if message == None or "" == message: 207 | return False 208 | if message != self.prev_values: 209 | logging.debug("Response changed:") 210 | logging.debug(f"- {self.prev_values}") 211 | logging.debug(f"+ {message}:") 212 | self.prev_values = message 213 | 214 | if len(message) < 38: 215 | logging.info("len message < 38: {}".format(len(message))) 216 | return False 217 | 218 | self.PowerDevice.entities.msg = message 219 | # if self.DeviceType == '12V100Ah-027': 220 | self.PowerDevice.entities.mvoltage = self.getValue(message, 0, 7) 221 | mcurrent = self.getValue(message, 8, 15) 222 | if mcurrent > 2147483647: 223 | mcurrent = mcurrent - 4294967295 224 | self.PowerDevice.entities.mcurrent = mcurrent 225 | self.PowerDevice.entities.mcapacity = self.getValue(message, 16, 23) 226 | self.PowerDevice.entities.charge_cycles = self.getValue(message, 24, 27) 227 | self.PowerDevice.entities.soc = self.getValue(message, 28, 31) 228 | self.PowerDevice.entities.temperature = self.getValue(message, 32, 35) 229 | self.PowerDevice.entities.status = self.getValue(message, 36, 37) 230 | self.PowerDevice.entities.afestatus = self.getValue(message, 40, 41) 231 | i = 0 232 | while i < 16: 233 | self.PowerDevice.entities.cell_mvoltage = (i + 1, self.getValue(message, (i * 4) + 44, (i * 4) + 47)) 234 | i = i + 1 235 | 236 | return True 237 | 238 | 239 | 240 | -------------------------------------------------------------------------------- /plugins/RenogyBatt/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import libscrc 3 | import dateutil.parser 4 | import re 5 | from datetime import datetime 6 | 7 | class Config(): 8 | NOTIFY_SERVICE_UUID = "0000fff0-0000-1000-8000-00805f9b34fb" 9 | NOTIFY_CHAR_UUID = "0000fff1-0000-1000-8000-00805f9b34fb" 10 | WRITE_SERVICE_UUID = "0000ffd0-0000-1000-8000-00805f9b34fb" 11 | WRITE_CHAR_UUID_POLLING = "0000ffd1-0000-1000-8000-00805f9b34fb" 12 | WRITE_CHAR_UUID_COMMANDS = "0000ffd1-0000-1000-8000-00805f9b34fb" 13 | SEND_ACK = True 14 | NEED_POLLING = True 15 | DEVICE_ID = 48 16 | 17 | class Util(): 18 | 19 | class VoltageCurrentSOCState(): 20 | REG_ADDR = 178 21 | READ_WORD = 6 22 | class TemperatureState(): 23 | REG_ADDR = 153 24 | READ_WORD = 7 25 | class CellVoltageState(): 26 | REG_ADDR = 136 27 | READ_WORD = 17 28 | 29 | def __init__(self, power_device): 30 | self.PowerDevice = power_device 31 | self.function_READ = 3 32 | self.function_WRITE = 6 33 | self.max_capacity = 0 34 | self.time_int = datetime.now() 35 | self.param_buffer = b"" 36 | self.param_expect = 0 37 | self.volt_change_count = 0 38 | self.param_data = [] 39 | self.poll_loop_count = 0 40 | self.poll_data = None 41 | self.poll_register = None 42 | 43 | def notificationUpdate(self, value, char): 44 | ''' 45 | Fortunately we read a different number of bytes from each register, so we can 46 | abuse the "length" field (byte #3 in the response) as an "id" 47 | ''' 48 | 49 | # extra BT val debugging 50 | # logging.debug("REG: {} VAL: {}".format(self.poll_register, value)) 51 | # logging.debug("Int vals are:") 52 | # for i in range(len(value)): 53 | # logging.debug("{}".format(value[i])) 54 | 55 | if not self.Validate(value): 56 | logging.warning("PollerUpdate - Invalid data: {}".format(value)) 57 | return False 58 | 59 | if value[0] == self.PowerDevice.device_id and value[1] == self.function_READ: 60 | if value[2] == self.VoltageCurrentSOCState.READ_WORD * 2: 61 | self.updateVoltageCurrentSOC(value) 62 | if value[2] == self.CellVoltageState.READ_WORD * 2: 63 | self.updateCellVoltage(value) 64 | if value[2] == self.TemperatureState.READ_WORD * 2: 65 | self.updateTemperature(value) 66 | elif value[0] == self.PowerDevice.device_id and value[1] == self.function_WRITE: 67 | # This is the first packet in a write-response 68 | # Ignore for now 69 | pass 70 | elif value[0] != self.PowerDevice.device_id and len(self.param_buffer) < self.param_expect: 71 | # Lets assume this is a follow up packet 72 | self.updateParamSettingData(value) 73 | else: 74 | logging.warning("Unknown packet received: {}".format(value)) 75 | return False 76 | 77 | return True 78 | 79 | 80 | def pollRequest(self, force = None): 81 | data = None 82 | self.poll_loop_count = self.poll_loop_count + 1 83 | if self.poll_loop_count == 1: 84 | data = self.create_poll_request('TotalCapacity') 85 | if self.poll_loop_count == 3: 86 | data = self.create_poll_request('VoltageCurrentSOC') 87 | elif self.poll_loop_count == 5: 88 | data = self.create_poll_request('CellVoltage') 89 | elif self.poll_loop_count == 6: 90 | data = self.create_poll_request('VoltageCurrentSOC') 91 | elif self.poll_loop_count == 7: 92 | data = self.create_poll_request('Temperature') 93 | elif self.poll_loop_count == 9: 94 | # Voltage and Current change more often, check these more 95 | data = self.create_poll_request('VoltageCurrentSOC') 96 | elif self.poll_loop_count == 11: 97 | data = self.create_poll_request('VoltageCurrentSOC') 98 | elif self.poll_loop_count == 13: 99 | data = self.create_poll_request('VoltageCurrentSOC') 100 | elif self.poll_loop_count == 15: 101 | data = self.create_poll_request('VoltageCurrentSOC') 102 | elif self.poll_loop_count == 16: 103 | # run TotalCapacity only once 104 | self.poll_loop_count = 2 105 | return data 106 | 107 | 108 | def ackData(self, value): 109 | return bytearray("main recv da ta[{0:02x}] [".format(value[0]), "ascii") 110 | 111 | def voltageToCapacity(self): 112 | # Hard-set the remaining capacity based on voltage to resync readings 113 | prev_capacity = self.PowerDevice.entities.exp_capacity 114 | new_voltage = self.PowerDevice.entities.voltage 115 | if new_voltage == 0: 116 | return 117 | if self.max_capacity == 0: 118 | return 119 | 120 | if prev_capacity != 0: 121 | # bypass logic, set initial capacity 122 | if abs(self.PowerDevice.entities.current) > 1.4: 123 | # Current makes Voltage to Capacity unreliable, return if Current too high 124 | if self.volt_change_count > 0: 125 | self.volt_change_count -= 1 126 | return 127 | 128 | # For crossover when charge + discharge happening, 129 | # current may be low, but volt still unreliable 130 | # therefore, set counter to ensure not tmp volt change 131 | self.volt_change_count += 1 132 | if self.volt_change_count < 300: 133 | return 134 | else: 135 | self.volt_change_count = 0 136 | 137 | percent = 100 138 | if new_voltage >= 13.5: 139 | percent = 100 140 | elif new_voltage >= 13.4: 141 | percent = 99 142 | elif new_voltage >= 13.3: 143 | percent = 90 144 | elif new_voltage >= 13.2: 145 | # special case for 13.2 since volt drop so small here 146 | if prev_capacity == 0: 147 | # new batt, no data, assume middle 148 | percent = 65 149 | elif (prev_capacity/self.max_capacity) > 80: 150 | percent = 80 151 | elif (prev_capacity/self.max_capacity) < 50: 152 | percent = 50 153 | else: 154 | percent = 65 155 | elif new_voltage >= 13.1: 156 | percent = 40 157 | elif new_voltage >= 13.0: 158 | percent = 30 159 | elif new_voltage >= 12.9: 160 | percent = 20 161 | elif new_voltage >= 12.0: 162 | percent = new_voltage * 10 - 111 163 | elif new_voltage >= 11.8: 164 | percent = (new_voltage * 10 - 100) / 2 - 1 165 | elif new_voltage >= 10.0: 166 | percent = (new_voltage * 10 - 100) / 2 167 | new_capacity = (self.max_capacity * percent)/100 168 | logging.debug("old capacity is {} and new is {}".format(prev_capacity, new_capacity)) 169 | if abs(prev_capacity - new_capacity)/self.max_capacity > .1: 170 | # reset only if dysnc is large - otherwise, we trust our reading 171 | self.PowerDevice.entities.exp_capacity = new_capacity 172 | return 173 | 174 | 175 | def updateVoltageCurrentSOC(self, bs): 176 | logging.debug("Voltage {} {} => {}".format( 177 | int(bs[5]), int(bs[6]), self.Bytes2Int(bs, 5, 2) * .1)) 178 | logging.debug("Current {} {} => {}".format( 179 | int(bs[3]), int(bs[4]), self.Bytes2Int(bs, 3, 2) * .01)) 180 | self.max_capacity = self.Bytes2Int(bs, 12, 3) * .001 181 | logging.debug("MaxCapacity {} {} {} => {}".format( 182 | int(bs[12]), int(bs[13]), int(bs[14]), self.max_capacity)) 183 | capacity = self.Bytes2Int(bs, 8, 3)* .001 184 | logging.debug("Capacity {} {} {} => {}. Divided by max for SOC: {}".format( 185 | int(bs[8]), int(bs[9]), int(bs[10]), capacity, 186 | (capacity/self.max_capacity * 100))) 187 | self.PowerDevice.entities.capacity = capacity 188 | self.PowerDevice.entities.soc = capacity/self.max_capacity * 100 189 | self.PowerDevice.entities.max_capacity = self.max_capacity 190 | 191 | current = self.Bytes2Int(bs, 3, 2) * .01 192 | if current > 255: 193 | current = current - 655.34 194 | self.PowerDevice.entities.current = current 195 | self.updateCapacityFromCurrent() 196 | self.PowerDevice.entities.voltage = self.Bytes2Int(bs, 5, 2) * .1 197 | # hard-set capacity based on voltage to reset desync 198 | self.voltageToCapacity() 199 | return 200 | 201 | 202 | def updateCellVoltage(self, bs): 203 | logging.debug("CellCount {}".format(int(bs[4]))) 204 | self.PowerDevice.entities.cell_count = int(bs[4]) 205 | for j in range(int(bs[4])): 206 | local_s = 5 + (j*2) 207 | logging.debug("CellmVoltage {} {} => {}".format( 208 | int(bs[local_s]),int(bs[local_s+1]), self.Bytes2Int(bs, local_s, 2) * 100)) 209 | self.PowerDevice.entities.cell_mvoltage = (j+1,self.Bytes2Int(bs, local_s, 2) * 100) 210 | return 211 | 212 | 213 | def updateTemperature(self, bs): 214 | logging.debug("TemperatureCount {}".format(int(bs[4]))) 215 | for j in range(int(bs[4])): 216 | local_s = 5 + (j*2) 217 | logging.debug("Temperature {} {} => {}".format( 218 | int(bs[local_s]),int(bs[local_s+1]), self.Bytes2Int(bs, local_s, 2) * .1)) 219 | temperature = self.Bytes2Int(bs, local_s, 2) * .1 220 | if temperature > 255: 221 | temperature = temperature - 6553.4 222 | self.PowerDevice.entities.temperature_celsius = temperature 223 | self.PowerDevice.entities.battery_temperature_celsius = temperature 224 | return 225 | 226 | # Remaining Capacity reading from battery could be... improved 227 | # Here we run our own updating of capacity. 228 | # Since voltage changes, we convert to watts (per second), 229 | # subtract, then convert back to amps 230 | def updateCapacityFromCurrent(self): 231 | # time since last update, we (unfortunately) assume same current whole time 232 | cur_time = datetime.now() 233 | charge_watts = self.PowerDevice.entities.current * self.PowerDevice.entities.voltage 234 | logging.debug("Seconds pass is {}".format((cur_time - self.time_int).total_seconds())) 235 | charge_watts = charge_watts * (cur_time - self.time_int).total_seconds() 236 | self.time_int = cur_time 237 | # capacity rounds for display, use mcapacity 238 | capacity_watts = (self.PowerDevice.entities.exp_capacity * 12.8 * 60 * 60) 239 | new_watts = capacity_watts + charge_watts 240 | capacity_amps = new_watts/(12.8 * 60 * 60) 241 | self.PowerDevice.entities.exp_capacity = capacity_amps 242 | return 243 | 244 | 245 | def Bytes2Int(self, bs, offset, length): 246 | # Reads data from a list of bytes, and converts to an int 247 | # Bytes2Int(bs, 3, 2) 248 | ret = 0 249 | if len(bs) < (offset + length): 250 | return ret 251 | if length > 0: 252 | # offset = 11, length = 2 => 11 - 12 253 | byteorder='big' 254 | start = offset 255 | end = offset + length 256 | else: 257 | # offset = 11, length = -2 => 10 - 11 258 | byteorder='little' 259 | start = offset + length + 1 260 | end = offset + 1 261 | # logging.debug("Reading byte {} to {} of string {}".format(start, end, bs)) 262 | # Easier to read than the bitshifting below 263 | return int.from_bytes(bs[start:end], byteorder=byteorder) 264 | 265 | i = 0 266 | s = offset + length - 1 267 | while s >= offset: 268 | # logging.debug("Reading from bs {} pos {}".format(bs, s)) 269 | # logging.debug("Value {}".format(bs[s])) 270 | # Start at the back, and read each byte, multiply with 256 i times for each new byte 271 | if i == 0: 272 | ret = bs[s] 273 | else: 274 | ret = ret + bs[s] * (256 * i) 275 | i = i + 1 276 | s = s - 1 277 | return ret 278 | ''' 279 | 280 | 281 | ret = 0 282 | i = 0 283 | while i < length: 284 | ret |= (bs[offset + i] & 255) << (((length - i) - 1) * 8) 285 | i += 1 286 | return ret 287 | ''' 288 | 289 | def Int2Bytes(self, i, pos = 0): 290 | # Converts an integer into 2 bytes (16 bits) 291 | # Returns either the first or second byte as an int 292 | if pos == 0: 293 | return int(format(i, '016b')[:8], 2) 294 | if pos == 1: 295 | return int(format(i, '016b')[8:], 2) 296 | return 0 297 | 298 | def Validate(self, bs): 299 | header = 3 300 | checksum = 2 301 | if bs == None or len(bs) < header + checksum: 302 | logging.warning("Invalid BS {}".format(bs)) 303 | return False 304 | 305 | function = bs[1] 306 | if function == 6: 307 | # Response to write-function. Ignore 308 | return True 309 | length = bs[2] 310 | if len(bs) - (header + checksum) != int(length): 311 | logging.warning("Invalid BS (wrong length) {}".format(bs)) 312 | return False 313 | 314 | crc = libscrc.modbus(bytes(bs[:-2])) 315 | check = self.Bytes2Int(bs, offset=len(bs)-1, length=-2) 316 | if crc == check: 317 | return True 318 | logging.warning("CRC Failed: {} - Check: {}".format(crc, check)) 319 | return False 320 | 321 | 322 | def create_poll_request(self, cmd): 323 | logging.debug("{} {}".format("create_poll_request", cmd)) 324 | data = None 325 | function = self.function_READ 326 | device_id = self.PowerDevice.device_id 327 | self.poll_register = cmd 328 | regAddr = 0 329 | if cmd == 'VoltageCurrentSOC': 330 | regAddr = self.VoltageCurrentSOCState.REG_ADDR 331 | readWrd = self.VoltageCurrentSOCState.READ_WORD 332 | elif cmd == 'Temperature': 333 | regAddr = self.TemperatureState.REG_ADDR 334 | readWrd = self.TemperatureState.READ_WORD 335 | elif cmd == 'CellVoltage': 336 | regAddr = self.CellVoltageState.REG_ADDR 337 | readWrd = self.CellVoltageState.READ_WORD 338 | 339 | if regAddr: 340 | data = [] 341 | data.append(self.PowerDevice.device_id) 342 | data.append(function) 343 | if cmd == 'TotalCapacity': 344 | data.append(20) 345 | else: 346 | data.append(19) 347 | data.append(regAddr) 348 | data.append(0) 349 | data.append(readWrd) 350 | crc = libscrc.modbus(bytes(data)) 351 | data.append(self.Int2Bytes(crc, 1)) 352 | data.append(self.Int2Bytes(crc, 0)) 353 | logging.debug("{} {} => {}".format("create_poll_request", cmd, data)) 354 | return data 355 | 356 | -------------------------------------------------------------------------------- /plugins/SolarLink/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import libscrc 3 | 4 | 5 | 6 | class Config(): 7 | NOTIFY_SERVICE_UUID = "0000fff0-0000-1000-8000-00805f9b34fb" 8 | NOTIFY_CHAR_UUID = "0000fff1-0000-1000-8000-00805f9b34fb" 9 | WRITE_SERVICE_UUID = "0000ffd0-0000-1000-8000-00805f9b34fb" 10 | WRITE_CHAR_UUID_POLLING = "0000ffd1-0000-1000-8000-00805f9b34fb" 11 | WRITE_CHAR_UUID_COMMANDS = "0000ffd1-0000-1000-8000-00805f9b34fb" 12 | SEND_ACK = True 13 | NEED_POLLING = True 14 | DEVICE_ID = 255 15 | 16 | class Util(): 17 | ''' 18 | Read data from SolarLink Regulators 19 | 20 | 21 | ''' 22 | 23 | class BatteryParamInfo(): 24 | REG_ADDR = 256 25 | READ_WORD = 7 26 | RESP_ID = 14 27 | class SolarPanelAndBatteryState(): 28 | REG_ADDR = 288 29 | READ_WORD = 3 30 | RESP_ID = 6 31 | class SolarPanelInfo(): 32 | REG_ADDR = 263 33 | READ_WORD = 4 34 | RESP_ID = 8 35 | class ParamSettingData(): 36 | REG_ADDR = 57345 37 | READ_WORD = 33 38 | RESP_ID = 66 39 | class RegulatorPower(): 40 | REG_ADDR = 266 41 | on = 1 42 | off = 0 43 | 44 | 45 | def __init__(self, power_device): 46 | self.PowerDevice = power_device 47 | self.function_READ = 3 48 | self.function_WRITE = 6 49 | 50 | 51 | self.param_buffer = b"" 52 | self.param_expect = 0 53 | self.param_data = [] 54 | self.poll_loop_count = 0 55 | self.poll_data = None 56 | self.poll_register = None 57 | 58 | def notificationUpdate(self, value, char): 59 | ''' 60 | Fortunately we read a different number of bytes from each register, so we can 61 | abuse the "length" field (byte #3 in the response) as an "id" 62 | ''' 63 | logging.debug("REG: {} VAL: {}".format(self.poll_register, value)) 64 | if not self.Validate(value): 65 | logging.warning("PollerUpdate - Invalid data: {}".format(value)) 66 | return False 67 | 68 | if value[0] == self.PowerDevice.device_id and value[1] == self.function_READ: 69 | # This is the first packet in a read-response 70 | if value[2] == self.BatteryParamInfo.READ_WORD * 2: 71 | self.updateBatteryParamInfo(value) 72 | if value[2] == self.SolarPanelAndBatteryState.READ_WORD * 2: 73 | self.updateSolarPanelAndBatteryState(value) 74 | if value[2] == self.SolarPanelInfo.READ_WORD * 2: 75 | self.updateSolarPanelInfo(value) 76 | if value[2] == self.ParamSettingData.READ_WORD * 2: 77 | self.updateParamSettingData(value) 78 | 79 | elif value[0] == self.PowerDevice.device_id and value[1] == self.function_WRITE: 80 | # This is the first packet in a write-response 81 | # Ignore for now 82 | pass 83 | elif value[0] != self.PowerDevice.device_id and len(self.param_buffer) < self.param_expect: 84 | # Lets assume this is a follow up packet 85 | self.updateParamSettingData(value) 86 | else: 87 | logging.warning("Unknown packet received: {}".format(value)) 88 | return False 89 | 90 | return True 91 | 92 | 93 | def pollRequest(self, force = None): 94 | data = None 95 | self.poll_loop_count = self.poll_loop_count + 1 96 | if self.poll_loop_count == 1: 97 | data = self.create_poll_request('BatteryParamInfo') 98 | if self.poll_loop_count == 3: 99 | data = self.create_poll_request('SolarPanelInfo') 100 | # if self.poll_loop_count == 5: 101 | # self.create_poll_request('SolarPanelAndBatteryState') 102 | # if self.poll_loop_count == 7: 103 | # self.create_poll_request('ParamSettingData') 104 | if self.poll_loop_count == 10: 105 | self.poll_loop_count = 0 106 | return data 107 | 108 | 109 | def ackData(self, value): 110 | return bytearray("main recv da ta[{0:02x}] [".format(value[0]), "ascii") 111 | 112 | 113 | def cmdRequest(self, command, value): 114 | cmd = None 115 | datas = [] 116 | logging.debug("{} {} => {}".format('cmdRequest', command, value)) 117 | if command == 'power_switch': 118 | if int(value) == 0: 119 | cmd = 'RegulatorPowerOff' 120 | elif int(value) == 1: 121 | cmd = 'RegulatorPowerOn' 122 | if cmd: 123 | datas.append(self.create_poll_request(cmd)) 124 | datas.append(self.create_poll_request('SolarPanelInfo')) 125 | datas.append(self.create_poll_request('BatteryParamInfo')) 126 | return datas 127 | 128 | 129 | 130 | 131 | def updateBatteryParamInfo(self, bs): 132 | logging.debug("mSOC {} {} => {} %".format(int(bs[3]), int(bs[4]), self.Bytes2Int(bs, 3, 2))) 133 | self.PowerDevice.entities.soc = self.Bytes2Int(bs, 3, 2) 134 | logging.debug("mVoltage {} {} => {} V".format(int(bs[5]), int(bs[6]), self.Bytes2Int(bs, 5, 2) * 0.1)) 135 | self.PowerDevice.entities.charge_voltage = self.Bytes2Int(bs, 5, 2) * 0.1 136 | logging.debug("mElectricity {} {} => {} A".format(int(bs[7]), int(bs[8]), self.Bytes2Int(bs, 7, 2) * 0.01)) 137 | self.PowerDevice.entities.charge_current = self.Bytes2Int(bs, 7, 2) * 0.01 138 | logging.debug("mDeviceTemperature {}".format(int(bs[9]))) 139 | temp_celsius = self.Bytes2Int(bs, 9, 1) 140 | if temp_celsius > 128: 141 | temp_celsius = 128 - temp_celsius 142 | self.PowerDevice.entities.temperature_celsius = temp_celsius 143 | logging.debug("mDeviceTemperatureCelsius {}".format(self.PowerDevice.entities.temperature_celsius)) 144 | battery_temp_celsius = self.Bytes2Int(bs, 10, 1) 145 | logging.debug("mBatteryTemperature {}".format(int(bs[10]))) 146 | if battery_temp_celsius > 128: 147 | battery_temp_celsius = 128 - battery_temp_celsius 148 | self.PowerDevice.entities.battery_temperature_celsius = battery_temp_celsius 149 | logging.debug("mBatteryTemperatureCelsius {}".format(self.PowerDevice.entities.battery_temperature_celsius)) 150 | logging.debug("mLoadVoltage {} {} => {} V".format(int(bs[11]), int(bs[12]), self.Bytes2Int(bs, 11, 2) * 0.1)) 151 | self.PowerDevice.entities.voltage = self.Bytes2Int(bs, 11, 2) * 0.1 152 | logging.debug("mLoadElectricity {} {} => {} A".format(int(bs[13]), int(bs[14]), self.Bytes2Int(bs, 13, 2) * 0.01)) 153 | self.PowerDevice.entities.current = self.Bytes2Int(bs, 13, 2) * 0.01 154 | logging.debug("mLoadPower {} {} => {} W".format(int(bs[15]), int(bs[16]), self.Bytes2Int(bs, 15, 2))) 155 | self.PowerDevice.entities.power = self.Bytes2Int(bs, 15, 2) 156 | return 157 | 158 | 159 | 160 | def updateSolarPanelAndBatteryState(self, bs): 161 | logging.debug("mSolarPanelState {} => {}".format(int(bs[3]), self.Bytes2Int(bs, 3, 1) >> 7)) 162 | logging.debug("mBatteryState {} => {}".format(int(bs[4]), self.Bytes2Int(bs, 4, 1))) 163 | logging.debug("mControllerInfo {} {} {} {} => {}".format(int(bs[5]), int(bs[6]), int(bs[7]), int(bs[8]), self.Bytes2Int(bs, 5, 4))) 164 | return 165 | 166 | def updateSolarPanelInfo(self, bs): 167 | logging.debug("mVoltage {} {} => {}".format(int(bs[3]), int(bs[4]), self.Bytes2Int(bs, 3, 2) * 0.1)) 168 | self.PowerDevice.entities.input_voltage = self.Bytes2Int(bs, 3, 2) * 0.1 169 | logging.debug("mElectricity {} {} => {}".format(int(bs[5]), int(bs[6]), self.Bytes2Int(bs, 5, 2) * 0.01)) 170 | self.PowerDevice.entities.input_current = self.Bytes2Int(bs, 5, 2) * 0.01 171 | logging.debug("mChargingPower {} {} => {}".format(int(bs[7]), int(bs[8]), self.Bytes2Int(bs, 7, 2))) 172 | self.PowerDevice.entities.input_power = self.Bytes2Int(bs, 7, 2) 173 | logging.debug("mSwitch {} {} => {}".format(int(bs[9]), int(bs[10]), self.Bytes2Int(bs, 9, 2))) 174 | self.PowerDevice.entities.power_switch = self.Bytes2Int(bs, 9, 2) 175 | logging.debug("mUnkown {} {} => {}".format(int(bs[11]), int(bs[12]), self.Bytes2Int(bs, 11, 2))) 176 | 177 | 178 | def updateParamSettingData(self, bs): 179 | i = 0 180 | header = 3 181 | checksum = 2 182 | if bs[0] == 255 and bs[1] == 3: 183 | i = 3 184 | self.param_data = [] 185 | self.param_buffer = b"" 186 | self.param_expect = bs[2] 187 | self.param_buffer = self.param_buffer + bs[i:] 188 | logging.debug("Param-buffer ({}): {}".format(len(self.param_buffer), self.param_buffer)) 189 | 190 | if len(self.param_buffer) == self.param_expect + header + checksum: 191 | while i < 66: 192 | self.param_data.append(int.from_bytes(self.param_buffer[i:i+2], byteorder='big')) 193 | i = i + 2 194 | logging.debug("ParamSettingData: {}".format(self.param_data)) 195 | ''' 196 | i = 0 197 | while i= 28: 203 | mLoadOptMod = self.mData[28] 204 | logging.debug("mLoadOptMod: {}".format(mLoadOptMod)) 205 | if mLoadOptMod == 15: 206 | logging.debug("Switch on") 207 | else: 208 | logging.debug("Switch off") 209 | ''' 210 | 211 | 212 | 213 | def Bytes2Int(self, bs, offset, length): 214 | # Reads data from a list of bytes, and converts to an int 215 | # Bytes2Int(bs, 3, 2) 216 | ret = 0 217 | if len(bs) < (offset + length): 218 | return ret 219 | if length > 0: 220 | # offset = 11, length = 2 => 11 - 12 221 | byteorder='big' 222 | start = offset 223 | end = offset + length 224 | else: 225 | # offset = 11, length = -2 => 10 - 11 226 | byteorder='little' 227 | start = offset + length + 1 228 | end = offset + 1 229 | # logging.debug("Reading byte {} to {} of string {}".format(start, end, bs)) 230 | # Easier to read than the bitshifting below 231 | return int.from_bytes(bs[start:end], byteorder=byteorder) 232 | 233 | i = 0 234 | s = offset + length - 1 235 | while s >= offset: 236 | # logging.debug("Reading from bs {} pos {}".format(bs, s)) 237 | # logging.debug("Value {}".format(bs[s])) 238 | # Start at the back, and read each byte, multiply with 256 i times for each new byte 239 | if i == 0: 240 | ret = bs[s] 241 | else: 242 | ret = ret + bs[s] * (256 * i) 243 | i = i + 1 244 | s = s - 1 245 | return ret 246 | ''' 247 | 248 | 249 | ret = 0 250 | i = 0 251 | while i < length: 252 | ret |= (bs[offset + i] & 255) << (((length - i) - 1) * 8) 253 | i += 1 254 | return ret 255 | ''' 256 | 257 | def Int2Bytes(self, i, pos = 0): 258 | # Converts an integer into 2 bytes (16 bits) 259 | # Returns either the first or second byte as an int 260 | if pos == 0: 261 | return int(format(i, '016b')[:8], 2) 262 | if pos == 1: 263 | return int(format(i, '016b')[8:], 2) 264 | return 0 265 | 266 | def Validate(self, bs): 267 | header = 3 268 | checksum = 2 269 | if bs == None or len(bs) < header + checksum: 270 | logging.warning("Invalid BS {}".format(bs)) 271 | return False 272 | 273 | function = bs[1] 274 | if function == 6: 275 | # Response to write-function. Ignore 276 | return True 277 | length = bs[2] 278 | if len(bs) - (header + checksum) != int(length): 279 | logging.warning("Invalid BS (wrong length) {}".format(bs)) 280 | return False 281 | 282 | 283 | # crc = libscrc.modbus(bytearray(bs[:-2])) 284 | crc = libscrc.modbus(bytes(bs[:-2])) 285 | check = self.Bytes2Int(bs, offset=len(bs)-1, length=-2) 286 | if crc == check: 287 | return True 288 | logging.warning("CRC Failed: {} - Check: {}".format(crc, check)) 289 | return False 290 | 291 | 292 | 293 | def create_poll_request(self, cmd): 294 | logging.debug("{} {}".format("create_poll_request", cmd)) 295 | data = None 296 | function = self.function_READ 297 | device_id = self.PowerDevice.device_id 298 | self.poll_register = cmd 299 | if cmd == 'SolarPanelAndBatteryState': 300 | regAddr = self.SolarPanelAndBatteryState.REG_ADDR 301 | readWrd = self.SolarPanelAndBatteryState.READ_WORD 302 | elif cmd == 'BatteryParamInfo': 303 | regAddr = self.BatteryParamInfo.REG_ADDR 304 | readWrd = self.BatteryParamInfo.READ_WORD 305 | elif cmd == 'SolarPanelInfo': 306 | regAddr = self.SolarPanelInfo.REG_ADDR 307 | readWrd = self.SolarPanelInfo.READ_WORD 308 | elif cmd == 'ParamSettingData': 309 | regAddr = self.ParamSettingData.REG_ADDR 310 | readWrd = self.ParamSettingData.READ_WORD 311 | elif cmd == 'RegulatorPowerOn': 312 | regAddr = self.RegulatorPower.REG_ADDR 313 | readWrd = self.RegulatorPower.on 314 | function = self.function_WRITE 315 | elif cmd == 'RegulatorPowerOff': 316 | regAddr = self.RegulatorPower.REG_ADDR 317 | readWrd = self.RegulatorPower.off 318 | function = self.function_WRITE 319 | 320 | if regAddr: 321 | data = [] 322 | data.append(self.PowerDevice.device_id) 323 | data.append(function) 324 | data.append(self.Int2Bytes(regAddr, 0)) 325 | data.append(self.Int2Bytes(regAddr, 1)) 326 | data.append(self.Int2Bytes(readWrd, 0)) 327 | data.append(self.Int2Bytes(readWrd, 1)) 328 | 329 | # crc = libscrc.modbus(bytearray(data)) 330 | crc = libscrc.modbus(bytes(data)) 331 | data.append(self.Int2Bytes(crc, 1)) 332 | data.append(self.Int2Bytes(crc, 0)) 333 | logging.debug("{} {} => {}".format("create_poll_request", cmd, data)) 334 | return data 335 | 336 | 337 | 338 | -------------------------------------------------------------------------------- /plugins/Topband/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # from __future__ import print_function 4 | import os 5 | import sys 6 | import time 7 | from datetime import datetime 8 | 9 | # import duallog 10 | import logging 11 | 12 | # duallog.setup('SmartPower', minLevel=logging.INFO) 13 | 14 | 15 | class Config(): 16 | SEND_ACK = False 17 | NEED_POLLING = False 18 | NOTIFY_SERVICE_UUID = "0000ffe0-0000-1000-8000-00805f9b34fb" 19 | NOTIFY_CHAR_UUID = "0000ffe4-0000-1000-8000-00805f9b34fb" 20 | 21 | class Util(): 22 | ''' 23 | Class for reading and parsing data from various Topband-Smartpower-BLE-streams 24 | ''' 25 | 26 | def __init__(self, power_device): 27 | self.protocolHead = 94 28 | self.protocolEnd = 0 29 | self.SOI = 1 30 | self.INFO = 2 31 | self.EOI = 3 32 | self.RecvDataType = self.SOI 33 | self.RevBuf = [None] * 115 34 | self.Revindex = 0 35 | # self.TAG = "SmartPowerUtil" 36 | self.PowerDevice = power_device 37 | self.end = 0 38 | 39 | 40 | 41 | def getValue(self, buf, start, end): 42 | # Reads "start" -> "end" from "buf" and return the hex-characters in the correct order 43 | string = buf[start:end + 1] 44 | # logging.debug(string) 45 | e = end + 1 46 | b = end - 1 47 | string = "" 48 | while b >= start: 49 | chrs = buf[b:e] 50 | # logging.debug(chrs) 51 | e = b 52 | b = b - 2 53 | string += chr(chrs[0]) + chr(chrs[1]) 54 | # logging.debug(string) 55 | try: 56 | ret = int(string, 16) 57 | except Exception as e: 58 | ret = 0 59 | return ret 60 | 61 | 62 | def asciitochar(self, a, b): 63 | x1 = 0 64 | if a >= 48 and a <= 57: 65 | x1 = a - 48 66 | elif a < 65 or a > 70: 67 | x1 = 0 68 | else: 69 | x1 = (a - 65) + 10 70 | x2 = x1 << 4 71 | if b >= 48 and b <= 57: 72 | return x2 + (b - 48) 73 | if b < 65 or b > 70: 74 | return x2 + 0 75 | return x2 + (b - 65) + 10 76 | 77 | 78 | def validateChecksum(self, buf): 79 | Chksum1 = 0 80 | Chksum2 = 0 81 | # end = 114 82 | j = 1 83 | while j < self.end - 5: 84 | # Chksum1 = int((self.asciitochar(buf[j], buf[j + 1]) + Chksum1)) 85 | Chksum1 = self.getValue(buf, j, j + 1) + Chksum1 86 | j += 2 87 | # logging.debug("Checksum 1: {}".format(Chksum1)) 88 | 89 | # Chksum2 = ((int(self.asciitochar(buf[j], buf[j + 1]))) << 8) + (int(self.asciitochar(buf[j + 2], buf[j + 3]))) 90 | Chksum2 = (self.getValue(buf, j, j + 1) << 8) + self.getValue(buf, j + 2, j + 3) 91 | 92 | # logging.debug("Checksum 2: {}".format(Chksum2)) 93 | # logging.info("C1 {} C2 {}".format(Chksum1, Chksum2)) 94 | if Chksum1 == Chksum2: 95 | return True 96 | return False 97 | 98 | 99 | def notificationUpdate(self, data, char): 100 | # Gets the binary data from the BLE-device and converts it to a list of hex-values 101 | cmdData = "" 102 | if data != None and len(data): 103 | i = 0 104 | while i < len(data): 105 | if self.Revindex > 114: 106 | self.Revindex = 0 107 | self.end = 0 108 | self.RecvDataType = self.SOI 109 | if self.RecvDataType == self.SOI: 110 | if data[i] == self.protocolHead: 111 | self.RecvDataType = self.INFO 112 | self.RevBuf[self.Revindex] = data[i] 113 | self.Revindex = self.Revindex + 1 114 | elif self.RecvDataType == self.INFO: 115 | self.RevBuf[self.Revindex] = data[i] 116 | self.Revindex = self.Revindex + 1 117 | 118 | if data[i] == self.protocolEnd: 119 | if self.end < 110: 120 | self.end = self.Revindex 121 | if self.Revindex == 114: 122 | self.RecvDataType = self.EOI 123 | elif self.RecvDataType == self.EOI: 124 | if self.validateChecksum(self.RevBuf): 125 | cmdData = self.RevBuf[1:self.Revindex] 126 | self.Revindex = 0 127 | self.end = 0 128 | self.RecvDataType = self.SOI 129 | return self.handleMessage(cmdData) 130 | self.Revindex = 0 131 | self.end = 0 132 | self.RecvDataType = self.SOI 133 | i += 1 134 | return False 135 | 136 | 137 | 138 | 139 | 140 | 141 | def handleMessage(self, message): 142 | # Accepts a list of hex-characters, and returns the human readable values into the powerDevice object 143 | logging.debug("handleMessage {}".format(message)) 144 | if message == None or "" == message: 145 | return False 146 | # logging.debug("test handleMessage == {}".format(message)) 147 | if len(message) < 38: 148 | logging.info("len message < 38: {}".format(len(message))) 149 | return False 150 | # logging.info("Parsing data from a {}".format(self.DeviceType)) 151 | 152 | self.PowerDevice.entities.msg = message 153 | # if self.DeviceType == '12V100Ah-027': 154 | self.PowerDevice.entities.mvoltage = self.getValue(message, 0, 7) 155 | logging.debug("mVoltage: {}".format(self.getValue(message, 0, 7))) 156 | mcurrent = self.getValue(message, 8, 15) 157 | if mcurrent > 2147483647: 158 | mcurrent = mcurrent - 4294967295 159 | self.PowerDevice.entities.mcurrent = mcurrent 160 | self.PowerDevice.entities.mcapacity = self.getValue(message, 16, 23) 161 | self.PowerDevice.entities.charge_cycles = self.getValue(message, 24, 27) 162 | self.PowerDevice.entities.soc = self.getValue(message, 28, 31) 163 | self.PowerDevice.entities.temperature = self.getValue(message, 32, 35) 164 | self.PowerDevice.entities.status = self.getValue(message, 36, 37) 165 | self.PowerDevice.entities.afestatus = self.getValue(message, 40, 41) 166 | i = 0 167 | while i < 16: 168 | self.PowerDevice.entities.cell_mvoltage = (i + 1, self.getValue(message, (i * 4) + 44, (i * 4) + 47)) 169 | i = i + 1 170 | 171 | return True -------------------------------------------------------------------------------- /plugins/VEDirect/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | 4 | class Config(): 5 | SEND_ACK = False 6 | NEED_POLLING = True 7 | NOTIFY_SERVICE_UUID = "306b0001-b081-4037-83dc-e59fcc3cdfd0" 8 | NOTIFY_CHAR_UUID = ["306b0002-b081-4037-83dc-e59fcc3cdfd0", 9 | "306b0003-b081-4037-83dc-e59fcc3cdfd0", 10 | "306b0004-b081-4037-83dc-e59fcc3cdfd0" 11 | ] 12 | WRITE_SERVICE_UUID = "306b0001-b081-4037-83dc-e59fcc3cdfd0" 13 | WRITE_CHAR_UUID_POLLING = "306b0002-b081-4037-83dc-e59fcc3cdfd0" 14 | WRITE_CHAR_UUID_COMMANDS = "306b0003-b081-4037-83dc-e59fcc3cdfd0" 15 | 16 | class Util(): 17 | # https://community.victronenergy.com/storage/attachments/2273-vecan-registers-public.pdf 18 | # 19 | # After reboot, you need to pair the device 20 | # Watch bluetoothctl for 21 | # [agent] Enter passkey (number in 0-999999): 000000 22 | # 23 | VREG_COMMANDS = { 24 | 'VREC': 0x0001, 25 | 'VACK': 0x0002, 26 | 'VPING': 0x0003, 27 | 'DEFAULTS': 0x0004, 28 | } 29 | # All ints are unsigned 30 | VREG_RESPONSES = { 31 | 'ProductID': { 'key': 0x0100, 'format': { 'id': 'un8', 'prodid': 'un16', 'flags': 'un8'}}, 32 | 'Revision': { 'key': 0x0101, 'format': { 'id': 'un8', 'revision': 'un16'}}, 33 | 'FwVersion': { 'key': 0x0102, 'format': { 'id': 'un8', 'fw': 'un24'}}, 34 | 'MinVersion': { 'key': 0x0103, 'format': { 'id': 'un8', 'fw': 'un24'}}, 35 | 'GroupID': { 'key': 0x0104, 'format': { 'groupid': 'un8'}}, 36 | 'HwRevision': { 'key': 0x0105, 'format': { 'hwrev': 'un8'}}, 37 | 'SerialNumber': { 'key': 0x010a, 'format': { 'serial': 'string32' }}, 38 | 'ModelName': { 'key': 0x010b, 'format': { 'model': 'string32' }}, 39 | 'Description1': { 'key': 0x010c, 'format': { 'model': 'string' }}, 40 | 'Description2': { 'key': 0x010d, 'format': { 'model': 'string' }}, 41 | 'Identify': { 'key': 0x010e, 'format': { 'identify': 'un8'}}, 42 | 'UdfVersion': { 'key': 0x0110, 'format': { 'version': 'un24', 'flags': 'un8' }}, 43 | 'Uptime': { 'key': 0x0120, 'format': { 'uptime': 'un32'}}, 44 | 'CanHwOverflows': { 'key': 0x0130, 'format': { 'overflows': 'un32'}}, 45 | 'CanSwOverflows': { 'key': 0x0131, 'format': { 'overflows': 'un32'}}, 46 | 'CanErrors': { 'key': 0x0132, 'format': { 'errors': 'un32'}}, 47 | 'CanBusOff': { 'key': 0x0133, 'format': { 'bussoff': 'un32'}}, 48 | } 49 | 50 | def __init__(self, power_device): 51 | self.PowerDevice = power_device 52 | self._char_buffer = b"" 53 | self._is_initialized = False 54 | self.poll_loop_count = 0 55 | 56 | def notificationUpdate(self, value, char): 57 | # Run when we receive a BLE-notification 58 | # print("[{}] Changed to {}".format(characteristic.uuid, value)) 59 | if char == "306b0004-b081-4037-83dc-e59fcc3cdfd0": 60 | self.set_bulk_values(char, value) 61 | elif char == "306b0003-b081-4037-83dc-e59fcc3cdfd0": 62 | self.set_values(value) 63 | else: 64 | logging.debug("[{}] Changed to {}".format(char, value)) 65 | self.set_values(value) 66 | return True 67 | 68 | def pollRequest(self): 69 | logging.debug("{} {} => {}".format('pollRequest', self.poll_loop_count, self._is_initialized)) 70 | data = None 71 | if not self._is_initialized and self.poll_loop_count == 2: 72 | self.send_magic_packets() 73 | self._is_initialized = True 74 | elif self.poll_loop_count == 5: 75 | self.keep_alive() 76 | self.poll_loop_count = 0 77 | # data = self.create_poll_request("PollData") 78 | self.poll_loop_count = self.poll_loop_count + 1 79 | return data 80 | # # Create a poll-request to ask for new data 81 | # c = charactersistcs["306b0002-b081-4037-83dc-e59fcc3cdfd0"] 82 | # hs = "f941" 83 | # b = bytearray.fromhex(hs) 84 | # c.write_value(b); 85 | 86 | 87 | def cmdRequest(self, command, value): 88 | # Create a command-request to run a command on the device 89 | cmd = None 90 | datas = [] 91 | logging.debug("{} {} => {}".format('cmdRequest', command, value)) 92 | if command == 'power_switch': 93 | if int(value) == 0: 94 | cmd = 'PowerOff' 95 | elif int(value) == 1: 96 | cmd = 'PowerOn' 97 | elif int(value) == 5: 98 | cmd = 'PowerEco' 99 | if cmd: 100 | datas.append(self.create_poll_request(cmd)) 101 | return datas 102 | 103 | 104 | def ackData(self): 105 | # Create an ack-packet 106 | pass 107 | 108 | def create_poll_request(self, cmd): 109 | logging.debug("{} {}".format("create_poll_request", cmd)) 110 | data = None 111 | if cmd == 'PollData': 112 | val = 'f941' 113 | if cmd == 'PowerOn': 114 | # val = "0603821902004105" # Eco instead of "on" 115 | val = "0603821902004102" 116 | if cmd == 'PowerOff': 117 | val = "0603821902004104" 118 | if cmd == 'PowerEco': 119 | val = "0603821902004105" 120 | if val: 121 | data = bytearray.fromhex(val) 122 | return data 123 | 124 | def send_magic_packets(self): 125 | # Some kind of magic session init 126 | # See also https://github.com/vvvrrooomm/victron/blob/947e6bd98cd184dec7cb38ecf47954c0391dfe8f/victron_smartsolar.py 127 | write_characteristic = self.PowerDevice.device_write_characteristic_polling 128 | 129 | hs = "fa80ff" 130 | value = bytearray.fromhex(hs) 131 | self.PowerDevice.characteristic_write_value(value, write_characteristic) 132 | time.sleep(0.1) 133 | 134 | hs = "f980" 135 | value = bytearray.fromhex(hs) 136 | self.PowerDevice.characteristic_write_value(value, write_characteristic) 137 | time.sleep(0.1) 138 | # c.write_value(b); 139 | 140 | # Send some data to the command-characteristics 141 | write_characteristic = self.PowerDevice.device_write_characteristic_commands 142 | 143 | hs = "01" 144 | value = bytearray.fromhex(hs) 145 | self.PowerDevice.characteristic_write_value(value, write_characteristic) 146 | time.sleep(0.1) 147 | 148 | hs = "0300" 149 | value = bytearray.fromhex(hs) 150 | self.PowerDevice.characteristic_write_value(value, write_characteristic) 151 | time.sleep(0.1) 152 | 153 | hs = "060082189342102703010303" 154 | value = bytearray.fromhex(hs) 155 | self.PowerDevice.characteristic_write_value(value, write_characteristic) 156 | time.sleep(0.1) 157 | 158 | # c = '306b0004-b081-4037-83dc-e59fcc3cdfd0' 159 | # hs = "05008119ec0f05008119ec0e05008119010c0500" 160 | # value = bytearray.fromhex(hs) 161 | # self.PowerDevice.characteristic_write_value(value, c) 162 | # time.sleep(0.1) 163 | 164 | hs = "81189005008119ec3f05008119ec12" 165 | value = bytearray.fromhex(hs) 166 | self.PowerDevice.characteristic_write_value(value, write_characteristic) 167 | time.sleep(0.1) 168 | 169 | hs = "19ecdc05038119eceb05038119eced" 170 | value = bytearray.fromhex(hs) 171 | self.PowerDevice.characteristic_write_value(value, write_characteristic) 172 | time.sleep(5) 173 | 174 | # Poll for data 175 | write_characteristic = self.PowerDevice.device_write_characteristic_polling 176 | hs = "f941" 177 | value = bytearray.fromhex(hs) 178 | self.PowerDevice.characteristic_write_value(value, write_characteristic) 179 | time.sleep(0.1) 180 | 181 | # Send some data to the command-characteristics 182 | write_characteristic = self.PowerDevice.device_write_characteristic_commands 183 | 184 | hs = "0600821893421027" 185 | value = bytearray.fromhex(hs) 186 | self.PowerDevice.characteristic_write_value(value, write_characteristic) 187 | time.sleep(5) 188 | 189 | # Poll for data 190 | write_characteristic = self.PowerDevice.device_write_characteristic_polling 191 | hs = "f941" 192 | value = bytearray.fromhex(hs) 193 | self.PowerDevice.characteristic_write_value(value, write_characteristic) 194 | time.sleep(0.1) 195 | 196 | 197 | 198 | def keep_alive(self): 199 | # ("0024", "0600821893421027"), 200 | # ("0021", "f941"), 201 | write_characteristic = self.PowerDevice.device_write_characteristic_commands 202 | hs = "060082189342102703010303" 203 | value = bytearray.fromhex(hs) 204 | self.PowerDevice.characteristic_write_value(value, write_characteristic) 205 | time.sleep(0.1) 206 | 207 | write_characteristic = self.PowerDevice.device_write_characteristic_polling 208 | hs = "f941" 209 | value = bytearray.fromhex(hs) 210 | self.PowerDevice.characteristic_write_value(value, write_characteristic) 211 | time.sleep(0.1) 212 | 213 | 214 | def validate(self): 215 | pass 216 | 217 | def set_bulk_values(self, char, value): 218 | start = int.from_bytes(value[0:2], byteorder="little") 219 | if start == 776: 220 | self._char_buffer = value 221 | else: 222 | self._char_buffer = self._char_buffer + value[:] 223 | if len(self._char_buffer) > 20: 224 | i = 0 225 | while i + 8 <= len(self._char_buffer): 226 | val = self._char_buffer[i:i+8] 227 | self.set_values(val) 228 | i = i + 8 229 | self._char_buffer = b"" 230 | 231 | 232 | 233 | def set_values(self, value): 234 | 235 | logging.debug(f"Got packet of len: {len(value)} {value}") 236 | if len(value) == 8: 237 | ptype = int.from_bytes(value[3:5], byteorder="little") 238 | pval = int.from_bytes(value[6:8], byteorder="little") 239 | logging.debug("8 Byte Data: {} {} {} {} {} {} {} {}".format( 240 | value[0], 241 | value[1], 242 | value[2], 243 | value[3], 244 | value[4], 245 | value[5], 246 | value[6], 247 | value[7])) 248 | logging.debug("ptype {}: pval {}".format(ptype, pval)) 249 | if ptype == 34: 250 | logging.debug("Output voltage: {} V".format(pval * 0.01)) 251 | self.PowerDevice.entities.voltage = pval * 0.01 252 | if self.PowerDevice.entities.voltage < 1: 253 | self.PowerDevice.entities.voltage = 0 254 | if pval > 10000: 255 | self.PowerDevice.entities.power_switch = 1 256 | # else: 257 | # self.PowerDevice.entities.power_switch = 0 258 | elif ptype == 36333: 259 | logging.debug("Input voltage: {} V".format(pval * 0.01)) 260 | self.PowerDevice.entities.input_voltage = pval * 0.01 261 | elif ptype == 36845: #current 262 | # 2^16 value 263 | # negative starts from 2^16 264 | # and goes down 265 | if pval > 2**16/2: # probably negative 266 | pval = 2**16 - pval 267 | pval *= -1 268 | logging.debug("Current: {} A".format(pval * 0.1)) 269 | self.PowerDevice.entities.current = pval * 0.1 270 | 271 | elif ptype == 290: 272 | if pval == 0: 273 | # logging.info("Output Power turned off #1") 274 | logging.debug("No output current") 275 | self.PowerDevice.entities.current = 0 276 | elif pval == 65535: 277 | logging.debug("Unknown data pval 65535: {}".format(value)) 278 | # self.PowerDevice.entities.power_switch = 1 279 | elif pval == 65534: 280 | logging.debug("Output Power turned on #2") 281 | self.PowerDevice.entities.power_switch = 1 282 | elif pval == 65533: 283 | # logging.info("Output Power ended") 284 | logging.debug("Checking output current") 285 | self.PowerDevice.entities.current = 0 286 | else: 287 | logging.debug("Current: {} A".format(pval * 0.1)) 288 | self.PowerDevice.entities.current = pval * 0.1 289 | else: 290 | logging.debug("Unknown-8 {}: {}".format(ptype, pval)) 291 | elif len(value) == 7: 292 | ptype = int(str(value[4]), 16) 293 | pval = int(str(value[6]), 16) 294 | state = "?" 295 | if ptype == 0: 296 | if pval == 2: 297 | logging.info("Output Power turned on") 298 | self.PowerDevice.entities.power_switch = 1 299 | if pval == 4: 300 | logging.info("Output Power turned off") 301 | self.PowerDevice.entities.power_switch = 0 302 | if pval == 5: 303 | logging.info("Output Power turned to eco") 304 | self.PowerDevice.entities.power_switch = 1 305 | elif ptype == 1: 306 | if pval == 0: 307 | logging.debug("Output Power state turned off ptype 1 pval 0") 308 | self.PowerDevice.entities.power_switch = 0 309 | if pval == 1: 310 | logging.debug("Output Power state turned to eco ptype 1 pval 1") 311 | self.PowerDevice.entities.power_switch = 1 312 | if pval == 9: 313 | logging.debug("Output Power state turned on ptype 1 pval 9") 314 | self.PowerDevice.entities.power_switch = 1 315 | else: 316 | logging.debug("Unknown-7 {}: {}".format(ptype, pval)) 317 | else: 318 | logging.debug("Unknown packet: {}".format(value)) 319 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | gatt 2 | configparser 3 | requests 4 | paho-mqtt 5 | libscrc 6 | python-dateutil 7 | dbus-python 8 | pycairo 9 | PyGObject==3.50.0 10 | -------------------------------------------------------------------------------- /solar-monitor.ini.dist: -------------------------------------------------------------------------------- 1 | [monitor] 2 | adapter = hci0 3 | debug = False 4 | temperature = C 5 | reconnect = False 6 | # C = Celsius 7 | # K = Kelvin 8 | # F = Farenheit 9 | 10 | 11 | [renogy_battery_1] 12 | type = RenogyBatt 13 | mac = 11:11:11:11:11:11 14 | reconnect = True 15 | 16 | [battery_1] 17 | type = Meritsun 18 | mac = 11:11:11:11:11:11 19 | reconnect = False 20 | 21 | [battery_2] 22 | type = Meritsun 23 | mac = 11:11:11:11:11:11 24 | reconnect = False 25 | 26 | [regulator] 27 | type = SolarLink 28 | mac = 11:11:11:11:11:11 29 | reconnect = False 30 | 31 | [inverter_1] 32 | type = VictronConnect 33 | mac = 11:11:11:11:11:11 34 | reconnect = False 35 | 36 | [rectifier_1] 37 | type = VictronConnect 38 | mac = 11:11:11:11:11:11 39 | reconnect = False 40 | 41 | 42 | [datalogger] 43 | url = http://server/solar/api/ 44 | token = 39129e20be0503937cb72a5f719337cc 45 | 46 | [mqtt] 47 | # Address to the mqtt broker 48 | broker = mqtt.server.addr 49 | # All topics are prefixed by this prefix 50 | # E.g. "solar/battery_1/voltage" 51 | prefix = solar 52 | 53 | 54 | -------------------------------------------------------------------------------- /solar-monitor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from argparse import ArgumentParser 4 | import configparser 5 | import time 6 | import logging 7 | import sys 8 | 9 | from solardevice import SolarDeviceManager, SolarDevice 10 | from datalogger import DataLogger 11 | import duallog 12 | 13 | import concurrent.futures 14 | import queue 15 | import threading 16 | 17 | 18 | 19 | 20 | 21 | # Read arguments 22 | arg_parser = ArgumentParser(description="Solar Monitor") 23 | arg_parser.add_argument( 24 | '--adapter', 25 | help="Name of Bluetooth adapter. Overrides what is set in .ini") 26 | arg_parser.add_argument( 27 | '-d', '--debug', action='store_true', 28 | help="Enable debug") 29 | arg_parser.add_argument( 30 | '--ini', 31 | help="Path to .ini-file. Defaults to 'solar-monior.ini'") 32 | args = arg_parser.parse_args() 33 | 34 | # Read config 35 | config = configparser.ConfigParser() 36 | 37 | ini_file = "solar-monitor.ini" 38 | if args.ini: 39 | ini_file = args.ini 40 | 41 | try: 42 | config.read(ini_file) 43 | except: 44 | print(f"Unable to read ini-file {ini_file}") 45 | sys.exit(1) 46 | 47 | 48 | if args.adapter: 49 | config.set('monitor', 'adapter', args.adapter) 50 | if args.debug: 51 | config.set('monitor', 'debug', '1') 52 | 53 | 54 | # Set up logging 55 | if config.getboolean('monitor', 'debug', fallback=False): 56 | print("Debug enabled") 57 | level = logging.DEBUG 58 | else: 59 | level = logging.INFO 60 | duallog.setup('solar-monitor', minLevel=level, fileLevel=level, rotation='daily', keep=30) 61 | 62 | # Create the message queue 63 | pipeline = queue.Queue(maxsize=10000) 64 | 65 | # Set up data logging 66 | # datalogger = None 67 | try: 68 | datalogger = DataLogger(config) 69 | except Exception as e: 70 | logging.error("Unable to set up datalogger") 71 | logging.error(e) 72 | sys.exit(1) 73 | 74 | def threaded_logger(queue, datalogger): 75 | x = time.time() 76 | try: 77 | while True: 78 | if not queue.empty(): 79 | y = time.time() 80 | if y > x + 1: 81 | logging.debug(f"Queue size = {queue.qsize()}") 82 | x = y 83 | logger_name, item, value = queue.get() 84 | datalogger.log(logger_name, item, value) 85 | except Exception as e: 86 | logging.error(e) 87 | sys.exit(299) 88 | 89 | executor = concurrent.futures.ThreadPoolExecutor(max_workers=100) 90 | 91 | executor.submit(threaded_logger, pipeline, datalogger) 92 | 93 | # Set up device manager and adapter 94 | device_manager = SolarDeviceManager(adapter_name=config['monitor']['adapter']) 95 | logging.info("Adapter status - Powered: {}".format(device_manager.is_adapter_powered)) 96 | if not device_manager.is_adapter_powered: 97 | logging.info("Powering on the adapter ...") 98 | device_manager.is_adapter_powered = True 99 | logging.info("Powered on") 100 | 101 | 102 | 103 | 104 | # Run discovery 105 | device_manager.update_devices() 106 | logging.info("Starting discovery...") 107 | # scan all the advertisements from the services list 108 | device_manager.start_discovery() 109 | discovering = True 110 | wait = 15 111 | found = [] 112 | # delay / sleep for 10 ~ 15 sec to complete the scanning 113 | while discovering: 114 | time.sleep(1) 115 | f = len(device_manager.devices()) 116 | logging.debug("Found {} BLE-devices so far".format(f)) 117 | found.append(f) 118 | if len(found) > 5: 119 | if found[len(found) - 5] == f: 120 | # We did not find any new devices the last 5 seconds 121 | discovering = False 122 | wait = wait - 1 123 | if wait == 0: 124 | discovering = False 125 | 126 | device_manager.stop_discovery() 127 | logging.info("Found {} BLE-devices".format(len(device_manager.devices()))) 128 | 129 | def threaded_poller(dev, device_manager, logger_name, config, datalogger, queue): 130 | logging.info("Trying to connect to {}...".format(dev.mac_address)) 131 | try: 132 | device = SolarDevice(mac_address=dev.mac_address, manager=device_manager, logger_name=logger_name, config=config, datalogger=datalogger, queue=queue) 133 | except Exception as e: 134 | logging.error(e) 135 | return 136 | device.connect() 137 | 138 | 139 | for dev in device_manager.devices(): 140 | logging.debug("Processing device {} {}".format(dev.mac_address, dev.alias())) 141 | for section in config.sections(): 142 | if config.get(section, "mac", fallback=None) and config.get(section, "type", fallback=None): 143 | mac = config.get(section, "mac").lower() 144 | if dev.mac_address.lower() == mac: 145 | executor.submit(threaded_poller, dev, device_manager, section, config, datalogger, pipeline) 146 | logging.info("Waiting for device to connect") 147 | time.sleep(1) 148 | 149 | logging.debug("Waiting for devices to connect...") 150 | time.sleep(10) 151 | logging.info("Terminate with Ctrl+C") 152 | try: 153 | device_manager.run() 154 | except KeyboardInterrupt: 155 | pass 156 | 157 | for dev in device_manager.devices(): 158 | try: 159 | dev.disconnect() 160 | except: 161 | pass 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /solar-monitor.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Solar Monitor 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | User=root 8 | WorkingDirectory=/home/pi/solar-monitor 9 | ExecStart=/home/pi/solar-monitor/solar-monitor.py 10 | RestartSec=13 11 | Restart=always 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | 16 | --------------------------------------------------------------------------------