├── LICENSE ├── README.md ├── custom_components └── cloud_gps │ ├── __init__.py │ ├── autoamap_data_fetcher.py │ ├── button.py │ ├── cmobd_data_fetcher.py │ ├── config_flow.py │ ├── const.py │ ├── device_tracker.py │ ├── gooddriver_data_fetcher.py │ ├── hellobike_data_fetcher.py │ ├── helper.py │ ├── manifest.json │ ├── niu_data_fetcher.py │ ├── sensor.py │ ├── switch.py │ ├── translations │ └── en.json │ ├── tuqiang123_data_fetcher.py │ └── tuqiangnet_data_fetcher.py └── hacs.json /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cloud_gps 2 | GPS定位平台数据接入homeassistant,包含“途强在线“、“途强物联”、“优驾盒子联网版”、“高德机车版”、“中移行车卫士”、"macless-haystack"等, 后续可能会加入等更多设备支持。 3 | 4 | # 注意: 5 | 近期发现2G设备网络很差,很多区域都没有信号了。现在全部重新购买4G或5G设备了。 6 | 7 | ## 安装方法: 8 | 9 | hacs安装: https://github.com/dscao/cloud_gps 10 | 11 | 手动安装: 将custom_components中的文件夹复制到ha中对应目录中 12 | 13 | 重启ha后,强制刷新浏览器一次,进入集成,搜索: cloud_gps或云平台GPS ,按提示配置即可。 14 | 15 | 搭配 https://github.com/dscao/gaode_maps 可比较简易实现显示国内地图和轨迹 16 | 17 | ## 说明 18 | 19 | 对于平台未提供地址信息的,可以使用api调用百度、高德或腾讯地图的接口来显示具体地址信息,可按喜好选用。 20 | 21 | 添加集成功后,第一步需要进入选项启用设备,才会出现实现。如果没有设备,说明账号中没有可支持的gps设备。 22 | 23 | 哈啰智能芯接入参数获取方法:https://github.com/louliangsheng/hellobike (现在好像没办法抓包了) 24 | 25 | macless-haystack 部署服务方法:https://gitee.com/lovelyelfpop/macless-haystack (此功能暂未发布,等测试一段时间) 26 | 27 | 中移行车卫士参数从小程序中抓包。 28 | 29 | 车辆状态属性或实体值为: 离线 -- [断电] -- 行驶 -- [钥匙开启]-- [震动]--停车,优先级依次降低。 [] 表示不支持的则不会出现。 30 | 31 | ![11](https://github.com/dscao/cloud_gps/assets/16587914/fb3d9a8b-b7f3-48ea-92be-a37c72b62c41) 32 | 33 | 34 | ![12](https://github.com/dscao/cloud_gps/assets/16587914/e9917c31-80d6-466c-9ad3-f234f939276a) 35 | 36 | 37 | ![13](https://github.com/dscao/cloud_gps/assets/16587914/adfec487-8eb7-48ba-b9e9-629cca131c3a) 38 | 39 | 40 | ![14](https://github.com/dscao/cloud_gps/assets/16587914/f58a39f1-e5a0-4be0-8f79-baa612761d53) 41 | 42 | 43 | ![PixPin_2025-05-29_15-04-33](https://github.com/user-attachments/assets/465cf494-c24e-4dac-a7cb-b85e59337fd1) 44 | 45 | 46 | ![PixPin_2025-05-29_15-06-23](https://github.com/user-attachments/assets/3ec7d828-84b1-4f2a-8105-9358fa9846b5) 47 | 48 | 49 | ![PixPin_2025-05-29_15-07-31](https://github.com/user-attachments/assets/0e7d46ef-46ef-4f71-9ebb-a06f92472d6b) 50 | 51 | -------------------------------------------------------------------------------- /custom_components/cloud_gps/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Support for cloud_gps 3 | Author : dscao 4 | Github : https://github.com/dscao 5 | Description : 6 | Date : 2023-11-16 7 | LastEditors : dscao 8 | LastEditTime : 2025-3-26 9 | ''' 10 | """ 11 | Component to integrate with Cloud_GPS. 12 | 13 | For more details about this component, please refer to 14 | https://github.com/dscao/cloud_gps 15 | """ 16 | import logging 17 | import asyncio 18 | import json 19 | import time, datetime 20 | import requests 21 | import re 22 | import hashlib 23 | import urllib.parse 24 | import math 25 | from importlib import import_module 26 | from aiohttp.client_exceptions import ClientConnectorError 27 | from async_timeout import timeout 28 | from dateutil.relativedelta import relativedelta 29 | import homeassistant.helpers.config_validation as cv 30 | import voluptuous as vol 31 | from homeassistant.components.sensor import PLATFORM_SCHEMA 32 | from requests import ReadTimeout, ConnectTimeout, HTTPError, Timeout, ConnectionError 33 | from datetime import timedelta 34 | import homeassistant.util.dt as dt_util 35 | from homeassistant.components import zone 36 | from homeassistant.components.device_tracker import PLATFORM_SCHEMA 37 | from homeassistant.components.device_tracker.const import CONF_SCAN_INTERVAL 38 | from homeassistant.components.device_tracker.legacy import DeviceScanner 39 | from homeassistant.core import HomeAssistant, callback 40 | from homeassistant.core_config import Config 41 | from homeassistant.config_entries import ConfigEntry 42 | from homeassistant.exceptions import ConfigEntryNotReady 43 | from homeassistant.helpers.aiohttp_client import async_get_clientsession 44 | from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed 45 | from homeassistant.helpers.event import async_track_time_interval 46 | from homeassistant.util import slugify 47 | from homeassistant.helpers.event import track_utc_time_change 48 | from homeassistant.util import slugify 49 | from homeassistant.util.location import distance 50 | from homeassistant.util.json import load_json 51 | from homeassistant.helpers.json import save_json 52 | from .helper import gcj02towgs84, wgs84togcj02, gcj02_to_bd09, bd09_to_gcj02, bd09_to_wgs84, wgs84_to_bd09 53 | 54 | from homeassistant.const import ( 55 | Platform, 56 | CONF_USERNAME, 57 | CONF_PASSWORD, 58 | ATTR_GPS_ACCURACY, 59 | ATTR_LATITUDE, 60 | ATTR_LONGITUDE, 61 | STATE_HOME, 62 | STATE_NOT_HOME, 63 | MAJOR_VERSION, 64 | MINOR_VERSION, 65 | ) 66 | 67 | from .const import ( 68 | COORDINATOR, 69 | DOMAIN, 70 | CONF_WEB_HOST, 71 | CONF_GPS_CONVER, 72 | CONF_DEVICE_IMEI, 73 | UNDO_UPDATE_LISTENER, 74 | CONF_ATTR_SHOW, 75 | CONF_UPDATE_ADDRESSDISTANCE, 76 | CONF_ADDRESSAPI, 77 | CONF_ADDRESSAPI_KEY, 78 | CONF_PRIVATE_KEY, 79 | CONF_UPDATE_INTERVAL, 80 | ) 81 | 82 | TYPE_GEOFENCE = "Geofence" 83 | __version__ = '2025.3.26' 84 | 85 | _LOGGER = logging.getLogger(__name__) 86 | 87 | PLATFORMS = [Platform.DEVICE_TRACKER, Platform.SENSOR, Platform.SWITCH, Platform.BUTTON] 88 | 89 | WAY_BAIDU = ["/directionlite/v1/driving","/directionlite/v1/riding","/directionlite/v1/walking","/directionlite/v1/transit"] 90 | WAY_GAODE = ["/v3/direction/driving","/v4/direction/bicycling","/v3/direction/walking","/v3/direction/transit/integrated"] 91 | WAY_QQ = ["/ws/direction/v1/driving/","/ws/direction/v1/bicycling/","/ws/direction/v1/walking/","/ws/direction/v1/transit/","/ws/direction/v1/ebicycling/"] 92 | TACTICS_BAIDU = [0,1,2,3,4,5] 93 | TACTICS_GAODE = [0,13,4,2,1,5] 94 | TACTICS_QQ = ["LEAST_TIME","AVOID_HIGHWAY","REAL_TRAFFIC","LEAST_TIME","LEAST_FEE","HIGHROAD_FIRST"] 95 | 96 | # 平台与模块映射关系 97 | PLATFORM_MODULE_MAP = { 98 | "gooddriver.cn": "gooddriver_data_fetcher", 99 | "tuqiang123.com": "tuqiang123_data_fetcher", 100 | "tuqiang.net": "tuqiangnet_data_fetcher", 101 | "cmobd.com": "cmobd_data_fetcher", 102 | "niu.com": "niu_data_fetcher", 103 | "hellobike.com": "hellobike_data_fetcher", 104 | "auto.amap.com": "autoamap_data_fetcher", 105 | } 106 | 107 | 108 | async def async_setup(hass: HomeAssistant, config: Config) -> bool: 109 | """Set up configured cloud_gps.""" 110 | hass.data.setdefault(DOMAIN, {}) 111 | return True 112 | 113 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 114 | """Set up cloud_gps as config entry.""" 115 | username = entry.data[CONF_USERNAME] 116 | password = entry.data[CONF_PASSWORD] 117 | webhost = entry.data[CONF_WEB_HOST] 118 | gps_conver = entry.options.get(CONF_GPS_CONVER, ["wgs84"]) 119 | device_imei = entry.options.get(CONF_DEVICE_IMEI, []) 120 | update_interval_seconds = entry.options.get(CONF_UPDATE_INTERVAL, 60) 121 | attr_show = entry.options.get(CONF_ATTR_SHOW, True) 122 | address_distance = entry.options.get(CONF_UPDATE_ADDRESSDISTANCE, 50) 123 | addressapi = entry.options.get(CONF_ADDRESSAPI, "none") 124 | api_key = entry.options.get(CONF_ADDRESSAPI_KEY, "") 125 | private_key = entry.options.get(CONF_PRIVATE_KEY, "") 126 | location_key = entry.unique_id 127 | 128 | # 异步导入模块 129 | try: 130 | module = await async_import_data_fetcher(hass, webhost) 131 | except (ValueError, ImportError) as e: 132 | raise ConfigEntryNotReady(str(e)) 133 | data_fetcher_class = module.DataFetcher 134 | _LOGGER.debug(device_imei) 135 | coordinator = CloudDataUpdateCoordinator( 136 | hass, data_fetcher_class, username, password, webhost, gps_conver, device_imei, location_key, update_interval_seconds, address_distance, addressapi, api_key, private_key 137 | ) 138 | 139 | await coordinator.async_refresh() 140 | 141 | if not coordinator.last_update_success: 142 | raise ConfigEntryNotReady 143 | 144 | undo_listener = entry.add_update_listener(update_listener) 145 | 146 | hass.data[DOMAIN][entry.entry_id] = { 147 | COORDINATOR: coordinator, 148 | UNDO_UPDATE_LISTENER: undo_listener, 149 | } 150 | 151 | await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) 152 | 153 | return True 154 | 155 | async def async_unload_entry(hass, entry): 156 | """Unload a config entry.""" 157 | unload_ok = all( 158 | await asyncio.gather( 159 | *[ 160 | hass.config_entries.async_forward_entry_unload(entry, component) 161 | for component in PLATFORMS 162 | ] 163 | ) 164 | ) 165 | 166 | hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() 167 | 168 | if unload_ok: 169 | hass.data[DOMAIN].pop(entry.entry_id) 170 | 171 | return unload_ok 172 | 173 | 174 | async def update_listener(hass, entry): 175 | """Update listener.""" 176 | await hass.config_entries.async_reload(entry.entry_id) 177 | 178 | async def async_import_data_fetcher(hass, webhost): 179 | """异步导入数据获取模块""" 180 | module_name = PLATFORM_MODULE_MAP.get(webhost) 181 | if not module_name: 182 | raise ValueError(f"Unsupported platform: {webhost}") 183 | 184 | try: 185 | return await hass.async_add_executor_job( 186 | lambda: import_module(f".{module_name}", __package__) 187 | ) 188 | except ImportError as e: 189 | _LOGGER.error("模块导入失败: %s", e) 190 | raise 191 | 192 | class CloudDataUpdateCoordinator(DataUpdateCoordinator): 193 | """Class to manage fetching cloud data API.""" 194 | 195 | def __init__(self, hass, data_fetcher_class, username, password, webhost, gps_conver, device_imei, location_key, update_interval_seconds, address_distance, addressapi, api_key, private_key): 196 | """Initialize.""" 197 | self._hass = hass 198 | update_interval = ( 199 | datetime.timedelta(seconds=int(update_interval_seconds)) 200 | ) 201 | _LOGGER.debug("Data %s , %s will be update every %s", webhost, device_imei, update_interval) 202 | 203 | super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) 204 | 205 | self._gps_conver = gps_conver 206 | self.device_imei = device_imei 207 | 208 | self._address_distance = address_distance 209 | self._addressapi = addressapi 210 | self._api_key = api_key 211 | self._private_key = private_key 212 | self.data = {} 213 | self._coords = {} 214 | self._coords_old = {} 215 | self._address = {} 216 | self._fetcher = data_fetcher_class(hass, username, password, device_imei, location_key) 217 | 218 | 219 | async def _async_update_data(self): 220 | """Update data via library.""" 221 | try: 222 | async with timeout(10): 223 | data = await self._fetcher.get_data() 224 | _LOGGER.debug("update_data: %s", data) 225 | _LOGGER.debug("gps_conver: %s", self._gps_conver) 226 | if self._gps_conver == "gcj02": 227 | for imei in self.device_imei: 228 | data[imei]["thislon"], data[imei]["thislat"] = gcj02towgs84(data[imei]["thislon"], data[imei]["thislat"]) 229 | if self._gps_conver == "bd09": 230 | for imei in self.device_imei: 231 | data[imei]["thislon"], data[imei]["thislat"] = bd09_to_wgs84(data[imei]["thislon"], data[imei]["thislat"]) 232 | for imei in self.device_imei: 233 | self._coords[imei] = [data[imei]["thislon"], data[imei]["thislat"]] 234 | if not self._coords_old.get(imei): 235 | self._coords_old[imei] = [0, 0] 236 | 237 | if self._addressapi != "none" and self._addressapi != None: 238 | for imei in self.device_imei: 239 | distance = self.get_distance(self._coords[imei][1], self._coords[imei][0], self._coords_old.get(imei)[1], self._coords_old.get(imei)[0]) 240 | if distance > self._address_distance: 241 | self._address[imei] = await self._get_address_frome_api(imei, self._addressapi, self._api_key, self._private_key) 242 | _LOGGER.debug("api_get_address: %s", self._address.get(imei)) 243 | data[imei]["attrs"]["address"] = self._address.get(imei) 244 | self.data = data 245 | except Exception as error: 246 | raise error 247 | return self.data 248 | 249 | 250 | async def _get_address_frome_api(self, imei, addressapi, api_key, private_key): 251 | try: 252 | async with timeout(5): 253 | if addressapi == "baidu" and api_key: 254 | _LOGGER.debug("baidu:"+api_key) 255 | addressdata = await self._hass.async_add_executor_job(self.get_baidu_geocoding, self._coords[imei][1], self._coords[imei][0], api_key, private_key) 256 | if addressdata['status'] == 0: 257 | self._coords_old[imei] = self._coords[imei] 258 | return addressdata['result']['formatted_address'] + addressdata['result']['sematic_description'] 259 | else: 260 | return addressdata['message'] 261 | elif addressapi == "gaode" and api_key: 262 | _LOGGER.debug("gaode:"+api_key) 263 | gcjdata = wgs84togcj02(self._coords[imei][0], self._coords[imei][1]) 264 | addressdata = await self._hass.async_add_executor_job(self.get_gaode_geocoding, gcjdata[1], gcjdata[0], api_key, private_key) 265 | if addressdata['status'] == "1": 266 | self._coords_old[imei] = self._coords[imei] 267 | return addressdata['regeocode']['formatted_address'] 268 | else: 269 | return addressdata['info'] 270 | 271 | elif addressapi == "tencent" and api_key: 272 | _LOGGER.debug("tencent:"+api_key) 273 | gcjdata = wgs84togcj02(self._coords[imei][0], self._coords[imei][1]) 274 | addressdata = await self._hass.async_add_executor_job(self.get_tencent_geocoding, gcjdata[1], gcjdata[0], api_key, private_key) 275 | if addressdata['status'] == 0: 276 | self._coords_old[imei] = self._coords[imei] 277 | return addressdata['result']['formatted_addresses']['recommend'] 278 | else: 279 | return addressdata['message'] 280 | elif addressapi == "free": 281 | _LOGGER.debug("free") 282 | gcjdata = wgs84togcj02(self._coords[imei][0], self._coords[imei][1]) 283 | bddata = gcj02_to_bd09(gcjdata[0], gcjdata[1]) 284 | addressdata = await self._hass.async_add_executor_job(self.get_free_geocoding, bddata[1], bddata[0]) 285 | if addressdata['status'] == 'OK': 286 | self._coords_old[imei] = self._coords[imei] 287 | return addressdata['result']['formatted_address'] 288 | else: 289 | return 'free接口返回错误' 290 | else: 291 | return "" 292 | except ClientConnectorError as error: 293 | return("连接错误: %s", error) 294 | except asyncio.TimeoutError: 295 | return("获取数据超时 (5秒)") 296 | except Exception as e: 297 | return("未知错误: %s", repr(e)) 298 | 299 | 300 | def get_data(self, url): 301 | json_text = requests.get(url).content 302 | json_text = json_text.decode('utf-8') 303 | json_text = re.sub(r'\\','',json_text) 304 | json_text = re.sub(r'"{','{',json_text) 305 | json_text = re.sub(r'}"','}',json_text) 306 | resdata = json.loads(json_text) 307 | return resdata 308 | 309 | def get_free_geocoding(self, lat, lng): 310 | api_url = 'https://api.map.baidu.com/geocoder' 311 | location = str("{:.6f}".format(lat))+','+str("{:.6f}".format(lng)) 312 | url = api_url+'?&output=json&location='+location 313 | _LOGGER.debug(url) 314 | response = self.get_data(url) 315 | _LOGGER.debug(response) 316 | return response 317 | 318 | def get_tencent_geocoding(self, lat, lng, api_key, private_key): 319 | api_url = 'https://apis.map.qq.com/ws/geocoder/v1/' 320 | location = str("{:.6f}".format(lat))+','+str("{:.6f}".format(lng)) 321 | sig = '' 322 | if private_key: 323 | params = '/ws/geocoder/v1/?get_poi=1&key='+api_key+'&location='+location+'&output=json' 324 | sig = self.tencent_sk(params, private_key) 325 | url = api_url+'?key='+api_key+'&output=json&get_poi=1&location='+location+'&sig='+sig 326 | _LOGGER.debug(url) 327 | response = self.get_data(url) 328 | _LOGGER.debug(response) 329 | return response 330 | 331 | def get_baidu_geocoding(self, lat, lng, api_key, private_key): 332 | api_url = 'https://api.map.baidu.com/reverse_geocoding/v3/' 333 | location = str("{:.6f}".format(lat))+','+str("{:.6f}".format(lng)) 334 | sn = '' 335 | if private_key: 336 | params = '/reverse_geocoding/v3/?ak='+api_key+'&output=json&coordtype=wgs84ll&extensions_poi=1&location='+location 337 | sn = self.baidu_sn(params, private_key) 338 | url = api_url+'?ak='+api_key+'&output=json&coordtype=wgs84ll&extensions_poi=1&location='+location+'&sn='+sn 339 | _LOGGER.debug(url) 340 | response = self.get_data(url) 341 | _LOGGER.debug(response) 342 | return response 343 | 344 | def get_gaode_geocoding(self, lat, lng, api_key, private_key): 345 | api_url = 'https://restapi.amap.com/v3/geocode/regeo' 346 | location = str("{:.6f}".format(lng))+','+str("{:.6f}".format(lat)) 347 | sig = '' 348 | if private_key: 349 | params = {'key': api_key, 'output': 'json', 'extensions': 'base', 'location': location} 350 | sig = self.generate_signature(params, private_key) 351 | url = api_url+'?key='+api_key+'&output=json&extensions=base&location='+location+'&sig='+sig 352 | _LOGGER.debug(url) 353 | response = self.get_data(url) 354 | _LOGGER.debug(response) 355 | return response 356 | 357 | def generate_signature(self, params, private_key): 358 | sorted_params = sorted(params.items(), key=lambda x: x[0]) # 按参数名的升序排序 359 | param_str = '&'.join([f'{key}={value}' for key, value in sorted_params]) # 构建参数字符串 360 | param_str += private_key # 加私钥 361 | signature = hashlib.md5(param_str.encode()).hexdigest() # 计算MD5摘要 362 | return signature #根据私钥计算出web服务数字签名 363 | 364 | def baidu_sn(self, params, private_key): 365 | param_str = urllib.parse.quote(params, safe="/:=&?#+!$,;'@()*[]") 366 | param_str += private_key 367 | signature = hashlib.md5(urllib.parse.quote_plus(param_str).encode()).hexdigest() 368 | return signature 369 | 370 | def tencent_sk(self, params, private_key): 371 | param_str = params + private_key 372 | signature = hashlib.md5(param_str.encode()).hexdigest() 373 | return signature 374 | 375 | def get_distance(self, lat1, lng1, lat2, lng2): 376 | earth_radius = 6378.137 377 | rad_lat1 = lat1 * math.pi / 180.0 378 | rad_lat2 = lat2 * math.pi / 180.0 379 | a = rad_lat1 - rad_lat2 380 | b = lng1 * math.pi / 180.0 - lng2 * math.pi / 180.0 381 | s = 2 * math.asin(math.sqrt(math.pow(math.sin(a / 2), 2) + math.cos(rad_lat1) * math.cos(rad_lat2) * math.pow(math.sin(b / 2), 2))) 382 | s = s * earth_radius 383 | return s * 1000 -------------------------------------------------------------------------------- /custom_components/cloud_gps/autoamap_data_fetcher.py: -------------------------------------------------------------------------------- 1 | """ 2 | get info 3 | """ 4 | 5 | import logging 6 | import requests 7 | import re 8 | import asyncio 9 | import json 10 | import time 11 | import datetime 12 | from async_timeout import timeout 13 | from aiohttp.client_exceptions import ClientConnectorError 14 | from homeassistant.helpers.aiohttp_client import async_create_clientsession 15 | from homeassistant.helpers.update_coordinator import UpdateFailed 16 | from urllib3.util.retry import Retry 17 | from requests.adapters import HTTPAdapter 18 | import math 19 | from homeassistant.const import ( 20 | CONF_USERNAME, 21 | CONF_PASSWORD, 22 | CONF_CLIENT_ID, 23 | ) 24 | 25 | from .const import ( 26 | COORDINATOR, 27 | DOMAIN, 28 | CONF_WEB_HOST, 29 | CONF_DEVICE_IMEI, 30 | UNDO_UPDATE_LISTENER, 31 | CONF_ATTR_SHOW, 32 | CONF_UPDATE_INTERVAL, 33 | ) 34 | 35 | _LOGGER = logging.getLogger(__name__) 36 | 37 | varstinydict = {} 38 | 39 | AUTOAMAP_API_HOST = "http://ts.amap.com/ws/tservice/internal/link/mobile/get?ent=2&in=" 40 | 41 | class DataFetcher: 42 | """fetch the cloud gps data""" 43 | 44 | def __init__(self, hass, username, password, device_imei, location_key): 45 | self.hass = hass 46 | self.location_key = location_key 47 | self.username = username 48 | self.password = password 49 | self.device_imei = device_imei 50 | self.session_autoamap = requests.session() 51 | self.userid = None 52 | self.usertype = None 53 | self.deviceinfo = {} 54 | self.trackerdata = {} 55 | self.address = {} 56 | self.lastgpstime = datetime.datetime.now() 57 | 58 | headers = { 59 | 'Host': 'ts.amap.com', 60 | 'Accept': 'application/json', 61 | 'sessionid': password.split("||")[1], 62 | 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', 63 | 'Cookie': 'sessionid=' + password.split("||")[1], 64 | } 65 | self.session_autoamap.headers.update(headers) 66 | 67 | global varstinydict 68 | _LOGGER.debug("varstinydict: %s", varstinydict) 69 | if not varstinydict.get("laststoptime_"+self.location_key): 70 | varstinydict["laststoptime_"+self.location_key] = "" 71 | if not varstinydict.get("lastlat_"+self.location_key): 72 | varstinydict["lastlat_"+self.location_key] = 0 73 | if not varstinydict.get("lastlon_"+self.location_key): 74 | varstinydict["lastlon_"+self.location_key] = 0 75 | if not varstinydict.get("isonline_"+self.location_key): 76 | varstinydict["isonline_"+self.location_key] = "离线" 77 | if not varstinydict.get("lastonlinetime_"+self.location_key): 78 | varstinydict["lastonlinetime_"+self.location_key] = "" 79 | if not varstinydict.get("lastofflinetime_"+self.location_key): 80 | varstinydict["lastofflinetime_"+self.location_key] = "" 81 | if not varstinydict.get("runorstop_"+self.location_key): 82 | varstinydict["runorstop_"+self.location_key] = "stop" 83 | if not varstinydict.get("course_"+self.location_key): 84 | varstinydict["course_"+self.location_key] = 0 85 | if not varstinydict.get("speed_"+self.location_key): 86 | varstinydict["speed_"+self.location_key] = 0 87 | 88 | 89 | 90 | def _get_devices_info(self): 91 | url = str.format(AUTOAMAP_API_HOST + self.password.split("||")[0]) 92 | p_data = self.password.split("||")[2] 93 | resp = self.session_autoamap.post(url, data=p_data).json()["data"]["carLinkInfoList"] 94 | return resp 95 | 96 | 97 | def time_diff(self, timestamp): 98 | result = datetime.datetime.now() - datetime.datetime.fromtimestamp(timestamp) 99 | hours = int(result.seconds / 3600) 100 | minutes = int(result.seconds % 3600 / 60) 101 | seconds = result.seconds%3600%60 102 | if result.days > 0: 103 | return("{0}天{1}小时{2}分钟".format(result.days,hours,minutes)) 104 | elif hours > 0: 105 | return("{0}小时{1}分钟".format(hours,minutes)) 106 | elif minutes > 0: 107 | return("{0}分钟{1}秒".format(minutes,seconds)) 108 | else: 109 | return("{0}秒".format(seconds)) 110 | 111 | 112 | def get_distance(self, lat1, lng1, lat2, lng2): 113 | earth_radius = 6378.137 114 | rad_lat1 = lat1 * math.pi / 180.0 115 | rad_lat2 = lat2 * math.pi / 180.0 116 | a = rad_lat1 - rad_lat2 117 | b = lng1 * math.pi / 180.0 - lng2 * math.pi / 180.0 118 | s = 2 * math.asin(math.sqrt(math.pow(math.sin(a / 2), 2) + math.cos(rad_lat1) * math.cos(rad_lat2) * math.pow(math.sin(b / 2), 2))) 119 | s = s * earth_radius 120 | return s * 1000 121 | 122 | def calculate_bearing(self, lat1, lng1, lat2, lng2): 123 | lat1 = math.radians(lat1) 124 | lat2 = math.radians(lat2) 125 | delta_lng = math.radians(lng2 - lng1) 126 | y = math.sin(delta_lng) * math.cos(lat2) 127 | x = math.cos(lat1) * math.sin(lat2) - math.sin(lat1) * math.cos(lat2) * math.cos(delta_lng) 128 | bearing = math.degrees(math.atan2(y, x)) 129 | return int((bearing + 360) % 360) 130 | 131 | 132 | 133 | async def get_data(self): 134 | 135 | try: 136 | async with timeout(10): 137 | devicesinfodata = await self.hass.async_add_executor_job(self._get_devices_info) 138 | except ClientConnectorError as error: 139 | _LOGGER.error("连接错误: %s", error) 140 | except asyncio.TimeoutError: 141 | _LOGGER.error("获取数据超时 (10秒)") 142 | except Exception as e: 143 | _LOGGER.error("未知错误: %s", repr(e)) 144 | finally: 145 | _LOGGER.debug("最终数据结果: %s", devicesinfodata) 146 | 147 | for imei in self.device_imei: 148 | _LOGGER.debug("get info imei: %s", imei) 149 | self.trackerdata[imei] = {} 150 | for infodata in devicesinfodata: 151 | if infodata.get("tid") == imei: 152 | self.deviceinfo[imei] = infodata 153 | self.deviceinfo[imei]["device_model"] = "高德地图车机版" 154 | self.deviceinfo[imei]["sw_version"] = infodata["sysInfo"]["autodiv"] 155 | 156 | querytime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 157 | thislat = infodata["naviLocInfo"]["lat"] 158 | thislon = infodata["naviLocInfo"]["lon"] 159 | 160 | distance = self.get_distance(thislat, thislon, varstinydict["lastlat_"+self.location_key], varstinydict["lastlon_"+self.location_key]) 161 | status = "停车" 162 | if distance > 10: 163 | _LOGGER.debug("状态为运动: %s ,%s ,%s", varstinydict,thislat,thislon) 164 | status = "行驶" 165 | distancetime = (datetime.datetime.now() - self.lastgpstime).total_seconds() 166 | if distancetime > 1 and distance < 10000: 167 | varstinydict["speed_"+self.location_key] = round((distance / distancetime * 3.6), 1) 168 | varstinydict["course_"+self.location_key] = self.calculate_bearing(thislat, thislon, varstinydict["lastlat_"+self.location_key], varstinydict["lastlon_"+self.location_key]) 169 | self.lastgpstime = datetime.datetime.now() 170 | varstinydict["runorstop_"+self.location_key] = "run" 171 | varstinydict["lastlat_"+self.location_key] = thislat 172 | varstinydict["lastlon_"+self.location_key] = thislon 173 | elif varstinydict["runorstop_"+self.location_key] == "run": 174 | _LOGGER.debug("变成静止: %s", varstinydict) 175 | status = "静止" 176 | varstinydict["laststoptime_"+self.location_key] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 177 | varstinydict["runorstop_"+self.location_key] = "stop" 178 | varstinydict["speed_"+self.location_key] = 0 179 | 180 | if infodata['naviStatus'] == 1: 181 | naviStatus = "导航中" 182 | status = "导航中" 183 | else: 184 | naviStatus = "未导航" 185 | 186 | if infodata["onlineStatus"] == 1: 187 | onlinestatus = "在线" 188 | elif infodata["onlineStatus"] == 0: 189 | onlinestatus = "离线" 190 | status = "离线" 191 | else: 192 | onlinestatus = "未知" 193 | 194 | if onlinestatus == "离线" and (varstinydict["isonline_"+self.location_key] == "在线"): 195 | varstinydict["lastofflinetime_"+self.location_key] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 196 | varstinydict["isonline_"+self.location_key] = "离线" 197 | if onlinestatus == "在线" and (varstinydict["isonline_"+self.location_key] == "离线"): 198 | varstinydict["lastonlinetime_"+self.location_key] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 199 | varstinydict["isonline_"+self.location_key] = "在线" 200 | 201 | lastofflinetime = varstinydict["lastofflinetime_"+self.location_key] 202 | lastonlinetime = varstinydict["lastonlinetime_"+self.location_key] 203 | onlinestatus = varstinydict["isonline_"+self.location_key] 204 | laststoptime = varstinydict["laststoptime_"+self.location_key] 205 | runorstop = varstinydict["runorstop_"+self.location_key] 206 | speed = varstinydict["speed_"+self.location_key] 207 | course = varstinydict["course_"+self.location_key] 208 | 209 | if laststoptime != "" and runorstop == "stop": 210 | parkingtime=self.time_diff(int(time.mktime(time.strptime(laststoptime, "%Y-%m-%d %H:%M:%S")))) 211 | else: 212 | parkingtime = "" 213 | 214 | attrs ={ 215 | "querytime": querytime, 216 | "speed": speed, 217 | "course": course, 218 | "distance": distance, 219 | "runorstop": runorstop, 220 | "laststoptime": laststoptime, 221 | "parkingtime": parkingtime, 222 | "naviStatus": naviStatus, 223 | "onlinestatus": onlinestatus, 224 | "lastofflinetime":lastofflinetime, 225 | "lastonlinetime":lastonlinetime 226 | } 227 | 228 | self.trackerdata[imei] = {"location_key":self.location_key+imei,"deviceinfo":self.deviceinfo[imei],"thislat":thislat,"thislon":thislon,"imei":imei,"status":status,"attrs":attrs} 229 | 230 | return self.trackerdata 231 | 232 | 233 | class GetDataError(Exception): 234 | """request error or response data is unexpected""" 235 | -------------------------------------------------------------------------------- /custom_components/cloud_gps/button.py: -------------------------------------------------------------------------------- 1 | """button Entities""" 2 | import logging 3 | import time 4 | import datetime 5 | import json 6 | import re 7 | import requests 8 | from async_timeout import timeout 9 | from aiohttp.client_exceptions import ClientConnectorError 10 | from homeassistant.helpers.update_coordinator import CoordinatorEntity 11 | from homeassistant.helpers.device_registry import DeviceEntryType 12 | from homeassistant.components.button import ( 13 | ButtonEntity, 14 | ButtonEntityDescription 15 | ) 16 | 17 | from homeassistant.const import ( 18 | CONF_USERNAME, 19 | CONF_PASSWORD, 20 | ) 21 | 22 | from .const import ( 23 | COORDINATOR, 24 | DOMAIN, 25 | CONF_WEB_HOST, 26 | CONF_BUTTONS, 27 | ) 28 | 29 | _LOGGER = logging.getLogger(__name__) 30 | 31 | HELLOBIKE_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.43(0x18002b2d) NetType/4G Language/zh_CN' 32 | API_URL_HELLOBIKE = "https://a.hellobike.com/evehicle/api" 33 | 34 | BUTTON_TYPES = { 35 | "bell": { 36 | "label": "bell", 37 | "name": "bell", 38 | "icon": "mdi:bell", 39 | "device_class": "restart", 40 | }, 41 | "nowtrack": { 42 | "label": "nowtrack", 43 | "name": "nowtrack", 44 | "icon": "mdi:map-marker-check", 45 | "device_class": "restart", 46 | } 47 | } 48 | 49 | 50 | BUTTON_TYPES_KEYS = {key for key, description in BUTTON_TYPES.items()} 51 | 52 | async def async_setup_entry(hass, config_entry, async_add_entities): 53 | """Add buttonentities from a config_entry.""" 54 | coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] 55 | webhost = config_entry.data[CONF_WEB_HOST] 56 | username = config_entry.data[CONF_USERNAME] 57 | password = config_entry.data[CONF_PASSWORD] 58 | enabled_buttons = [s for s in config_entry.options.get(CONF_BUTTONS, []) if s in BUTTON_TYPES_KEYS] 59 | 60 | _LOGGER.debug("coordinator buttons: %s", coordinator.data) 61 | _LOGGER.debug("enabled_buttons: %s" ,enabled_buttons) 62 | for coordinatordata in coordinator.data: 63 | _LOGGER.debug("coordinatordata") 64 | _LOGGER.debug(coordinatordata) 65 | buttons = [] 66 | for button_type in enabled_buttons: 67 | _LOGGER.debug("button_type: %s" ,button_type) 68 | buttons.append(CloudGPSButtonEntity(hass, webhost, username, password, coordinatordata, BUTTON_TYPES[button_type], coordinator)) 69 | 70 | async_add_entities(buttons, False) 71 | 72 | 73 | class CloudGPSButtonEntity(ButtonEntity): 74 | """Define an button entity.""" 75 | _attr_has_entity_name = True 76 | 77 | def __init__(self, hass, webhost, username, password, imei, description, coordinator): 78 | """Initialize.""" 79 | super().__init__() 80 | self._attr_icon = description['icon'] 81 | self._hass = hass 82 | self._description = description 83 | self.session_hellobike = requests.session() 84 | self._webhost = webhost 85 | self._username = username 86 | self._password = password 87 | self._imei = imei 88 | self.coordinator = coordinator 89 | _LOGGER.debug("ButtonEntity coordinator: %s", coordinator.data) 90 | self._unique_id = f"{self.coordinator.data[self._imei]['location_key']}-{description['label']}" 91 | self._attr_translation_key = f"{description['name']}" 92 | self._state = None 93 | if webhost == "tuqiang123.com": 94 | from .tuqiang123_data_fetcher import DataButton 95 | elif webhost == "hellobike.com": 96 | from .hellobike_data_fetcher import DataButton 97 | else: 98 | _LOGGER.error("配置的实体平台不支持,请不要启用此按钮实体!") 99 | return 100 | 101 | self._button = DataButton(hass, username, password, imei) 102 | 103 | 104 | @property 105 | def unique_id(self): 106 | return self._unique_id 107 | 108 | @property 109 | def device_info(self): 110 | """Return the device info.""" 111 | return { 112 | "identifiers": {(DOMAIN, self.coordinator.data[self._imei]["location_key"])}, 113 | "name": self._imei, 114 | "manufacturer": self._webhost, 115 | "entry_type": DeviceEntryType.SERVICE, 116 | "model": self.coordinator.data[self._imei]["deviceinfo"]["device_model"], 117 | "sw_version": self.coordinator.data[self._imei]["deviceinfo"]["sw_version"], 118 | } 119 | 120 | @property 121 | def should_poll(self): 122 | """Return the polling requirement of the entity.""" 123 | return True 124 | 125 | @property 126 | def state(self): 127 | """Return the state.""" 128 | return self._state 129 | 130 | @property 131 | def available(self): 132 | """Return the available.""" 133 | attr_available = True if (self.coordinator.data.get(self._imei, {}).get("attrs", {}).get("onlinestatus", "") == "在线" ) else False 134 | return attr_available 135 | 136 | @property 137 | def device_class(self): 138 | """Return the unit_of_measurement.""" 139 | if self._description.get("device_class"): 140 | return self._description["device_class"] 141 | 142 | 143 | def press(self) -> None: 144 | """Handle the button press.""" 145 | 146 | async def async_press(self) -> None: 147 | """Handle the button press.""" 148 | if self._webhost == "hellobike.com" and self._description['label']=="bell": 149 | self._state = await self._button._action("rent.order.bell") 150 | elif self._webhost == "tuqiang123.com" and self._description['label']=="nowtrack": 151 | self._state = await self._button._action("立即定位") 152 | 153 | 154 | async def async_added_to_hass(self): 155 | """Connect to dispatcher listening for entity data notifications.""" 156 | self.async_on_remove( 157 | self.coordinator.async_add_listener(self.async_write_ha_state) 158 | ) 159 | 160 | async def async_update(self): 161 | """Update entity.""" 162 | #await self.coordinator.async_request_refresh() 163 | 164 | 165 | -------------------------------------------------------------------------------- /custom_components/cloud_gps/cmobd_data_fetcher.py: -------------------------------------------------------------------------------- 1 | """ 2 | get info 3 | """ 4 | 5 | import logging 6 | import requests 7 | import re 8 | import asyncio 9 | import json 10 | import time 11 | import datetime 12 | import hashlib 13 | from async_timeout import timeout 14 | from aiohttp.client_exceptions import ClientConnectorError 15 | from homeassistant.helpers.aiohttp_client import async_create_clientsession 16 | from homeassistant.helpers.update_coordinator import UpdateFailed 17 | from urllib3.util.retry import Retry 18 | from requests.adapters import HTTPAdapter 19 | from homeassistant.const import ( 20 | CONF_USERNAME, 21 | CONF_PASSWORD, 22 | CONF_CLIENT_ID, 23 | ) 24 | 25 | from .const import ( 26 | COORDINATOR, 27 | DOMAIN, 28 | CONF_WEB_HOST, 29 | CONF_DEVICE_IMEI, 30 | UNDO_UPDATE_LISTENER, 31 | CONF_ATTR_SHOW, 32 | CONF_UPDATE_INTERVAL, 33 | ) 34 | 35 | _LOGGER = logging.getLogger(__name__) 36 | 37 | CMOBD_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.43(0x18002b2d) NetType/4G Language/zh_CN' 38 | CMOBD_API_URL = "https://lsapp.cmobd.com/v360/iovsaas" 39 | 40 | class DataFetcher: 41 | """fetch the cloud gps data""" 42 | 43 | def __init__(self, hass, username, password, device_imei, location_key): 44 | self.hass = hass 45 | self.location_key = location_key 46 | self.username = username 47 | self.password = password 48 | self.device_imei = device_imei 49 | self.session_cmobd = requests.session() 50 | self.cloudpgs_token = None 51 | self._lat_old = 0 52 | self._lon_old = 0 53 | self.deviceinfo = {} 54 | self.trackerdata = {} 55 | self.address = {} 56 | self.totalkm = {} 57 | 58 | headers = { 59 | 'Host': 'lsapp.cmobd.com', 60 | 'agent': 'Lushang/5.0.0', 61 | 'Cookie': 'node-ls-api=' + password, 62 | 'content-type': 'application/json', 63 | 'User-Agent': CMOBD_USER_AGENT, 64 | 'Referer': 'https://servicewechat.com/wx351871af12293380/31/page-frame.html' 65 | } 66 | self.session_cmobd.headers.update(headers) 67 | 68 | def _is_json(self, jsonstr): 69 | try: 70 | json.loads(jsonstr) 71 | except ValueError: 72 | return False 73 | return True 74 | 75 | def md5_hash(self, text): 76 | md5 = hashlib.md5() 77 | md5.update(text.encode('utf-8')) 78 | encrypted_text = md5.hexdigest() 79 | return encrypted_text 80 | 81 | def _devicelist_cmobd(self, token): 82 | url = CMOBD_API_URL 83 | p_data = { 84 | "cmd":"userVehicles", 85 | "ver":1, 86 | "token": token, 87 | "pageNo":0, 88 | "pageSize":10 89 | } 90 | resp = self.session_cmobd.post(url, data=p_data).json() 91 | return resp 92 | 93 | def _get_device_tracker(self, token, vehicleid): 94 | url = CMOBD_API_URL 95 | p_data = { 96 | "cmd": "weappVehicleRunStatus", 97 | "ver": 1, 98 | "token": token, 99 | "vehicleId": vehicleid, 100 | "isNeedGps": "1", 101 | "gpsStartTime": "" 102 | } 103 | resp = self.session_cmobd.post(url, data=p_data).json() 104 | return resp 105 | 106 | def time_diff(self, timestamp): 107 | result = datetime.datetime.now() - datetime.datetime.fromtimestamp(timestamp) 108 | hours = int(result.seconds / 3600) 109 | minutes = int(result.seconds % 3600 / 60) 110 | seconds = result.seconds%3600%60 111 | if result.days > 0: 112 | return("{0}天{1}小时{2}分钟".format(result.days,hours,minutes)) 113 | elif hours > 0: 114 | return("{0}小时{1}分钟".format(hours,minutes)) 115 | elif minutes > 0: 116 | return("{0}分钟{1}秒".format(minutes,seconds)) 117 | else: 118 | return("{0}秒".format(seconds)) 119 | 120 | async def get_data(self): 121 | 122 | if self.deviceinfo == {}: 123 | deviceslistinfo = await self.hass.async_add_executor_job(self._devicelist_cmobd, self.password) 124 | _LOGGER.debug("deviceslistinfo: %s", deviceslistinfo) 125 | if deviceslistinfo.get("result") != 0: 126 | _LOGGER.error("请求api错误: %s", deviceslistinfo.get("note")) 127 | return 128 | for deviceinfo in deviceslistinfo["dataList"]: 129 | self.deviceinfo[str(deviceinfo["vehicleID"])] = {} 130 | for deviceinfo in deviceslistinfo["dataList"]: 131 | self.deviceinfo[str(deviceinfo["vehicleID"])]["device_model"] = "中移行车卫士" + deviceinfo["deviceList"][0]["deviceTypeName"] 132 | self.deviceinfo[str(deviceinfo["vehicleID"])]["sw_version"] = deviceinfo["deviceList"][0]["modelName"] 133 | self.deviceinfo[str(deviceinfo["vehicleID"])]["expiration"] = "永久" 134 | 135 | 136 | for imei in self.device_imei: 137 | _LOGGER.debug("Requests vehicleID: %s", imei) 138 | self.trackerdata[imei] = {} 139 | 140 | try: 141 | async with timeout(10): 142 | data = await self.hass.async_add_executor_job(self._get_device_tracker, self.password, imei) 143 | except ClientConnectorError as error: 144 | _LOGGER.error("连接错误: %s", error) 145 | except asyncio.TimeoutError: 146 | _LOGGER.error("获取数据超时 (10秒)") 147 | except Exception as e: 148 | _LOGGER.error("未知错误: %s", repr(e)) 149 | finally: 150 | _LOGGER.debug("最终数据结果: %s", data) 151 | 152 | if data.get("result") == 0: 153 | querytime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 154 | updatetime = data.get("sampleTime") 155 | speed = float(data.get("vehicleSpeed", 0)) 156 | course = data.get("posDirection", 0) 157 | address = data.get("realLocation","") 158 | battery = int(data.get("soc", 0))/10 159 | 160 | status = "停车" 161 | 162 | if data["vehicleStatus"] == "1": 163 | acc = "钥匙开启" 164 | status = "钥匙开启" 165 | elif data["vehicleStatus"] == "0": 166 | acc = "钥匙关闭" 167 | else: 168 | acc = "未知" 169 | 170 | 171 | thislat = float(data["posLatitude"]) 172 | thislon = float(data["posLongitude"]) 173 | 174 | if data["stopTime"]: 175 | laststoptime = data["stopTime"] 176 | parkingtime = self.time_diff(int(time.mktime(time.strptime(data["stopTime"], "%Y-%m-%d %H:%M:%S")))) 177 | else: 178 | laststoptime = None 179 | parkingtime = "" 180 | 181 | if speed == 0: 182 | runorstop = "静止" 183 | else: 184 | runorstop = "运动" 185 | status = "行驶" 186 | 187 | if data["onlineStatus"] == "2": 188 | onlinestatus = "在线" 189 | elif data["onlineStatus"] == "1": 190 | onlinestatus = "待机" 191 | else: 192 | onlinestatus = "离线" 193 | status = "离线" 194 | 195 | if data["powerStatus"] != "0": 196 | status = "外电已断开" 197 | 198 | attrs = { 199 | "speed":speed, 200 | "course":course, 201 | "querytime":querytime, 202 | "laststoptime":laststoptime, 203 | "last_update":updatetime, 204 | "runorstop":runorstop, 205 | "acc":acc, 206 | "parkingtime":parkingtime, 207 | "address":address, 208 | "onlinestatus":onlinestatus, 209 | "battery":battery 210 | } 211 | 212 | self.trackerdata[imei] = {"location_key":self.location_key+str(imei),"deviceinfo":self.deviceinfo[imei],"thislat":thislat,"thislon":thislon,"status":status,"attrs":attrs} 213 | 214 | return self.trackerdata 215 | 216 | 217 | class GetDataError(Exception): 218 | """request error or response data is unexpected""" 219 | -------------------------------------------------------------------------------- /custom_components/cloud_gps/config_flow.py: -------------------------------------------------------------------------------- 1 | """Adds config flow for cloud.""" 2 | import logging 3 | import asyncio 4 | import json 5 | import time, datetime 6 | import requests 7 | import re 8 | import hashlib 9 | import homeassistant.helpers.config_validation as cv 10 | from homeassistant.const import CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_CLIENT_ID 11 | from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig, SelectSelectorMode 12 | from collections import OrderedDict 13 | from homeassistant import config_entries 14 | from homeassistant.core import callback 15 | 16 | from .const import ( 17 | CONF_GPS_CONVER, 18 | CONF_UPDATE_INTERVAL, 19 | CONF_ATTR_SHOW, 20 | PWD_NOT_CHANGED, 21 | DOMAIN, 22 | CONF_WEB_HOST, 23 | CONF_DEVICES, 24 | CONF_DEVICE_IMEI, 25 | CONF_SENSORS, 26 | CONF_SWITCHS, 27 | CONF_BUTTONS, 28 | KEY_QUERYTIME, 29 | KEY_PARKING_TIME, 30 | KEY_LASTSTOPTIME, 31 | KEY_ADDRESS, 32 | KEY_SPEED, 33 | KEY_TOTALKM, 34 | KEY_STATUS, 35 | KEY_ACC, 36 | KEY_BATTERY, 37 | CONF_UPDATE_ADDRESSDISTANCE, 38 | CONF_ADDRESSAPI, 39 | CONF_ADDRESSAPI_KEY, 40 | CONF_PRIVATE_KEY, 41 | CONF_WITH_MAP_CARD, 42 | ) 43 | 44 | import voluptuous as vol 45 | 46 | USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36' 47 | USER_AGENT_CMOBD = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.43(0x18002b2d) NetType/4G Language/zh_CN' 48 | USER_AGENT_NIU = 'manager/4.6.48 (android; IN2020 11);lang=zh-CN;clientIdentifier=Domestic;timezone=Asia/Shanghai;model=IN2020;deviceName=IN2020;ostype=android' 49 | USER_AGENT_GOODDRIVER = 'gooddriver/7.9.1 CFNetwork/1410.0.3 Darwin/22.6.0' 50 | 51 | 52 | WEBHOST = { 53 | "tuqiang123.com": "途强在线", 54 | "tuqiang.net": "途强物联", 55 | "gooddriver.cn": "优驾盒子联网版", 56 | "niu.com": "小牛电动车(暂未调试)", 57 | "cmobd.com": "中移行车卫士(*密码填写token)", 58 | "hellobike.com": "哈啰智能芯(*密码填写token)", 59 | "auto.amap.com": "高德车机版(*密码填写 Key||sessionid||paramdata)" 60 | } 61 | 62 | API_HOST_TUQIANG123 = "https://www.tuqiang123.com" # https://www.tuqiangol.com 或者 https://www.tuqiang123.com 63 | API_HOST_TUQIANGNET = "https://www.tuqiang.net" 64 | API_HOST_TOKEN_GOODDRIVER = "https://ssl.gooddriver.cn" # "https://ssl.gooddriver.cn" 或者 "http://121.41.101.95:8080" 65 | API_URL_GOODDRIVER = "http://restcore.gooddriver.cn/API/Values/HudDeviceDetail/" 66 | API_HOST_TOKEN_NIU = "https://account.niu.com" 67 | API_URL_NIU = "https://app-api.niu.com" 68 | API_URL_CMOBD = "https://lsapp.cmobd.com/v360/iovsaas" 69 | API_URL_HELLOBIKE = "https://a.hellobike.com/evehicle/api" 70 | API_URL_AUTOAMAP = "http://ts.amap.com/ws/tservice/internal/link/mobile/get?ent=2&in=" 71 | 72 | _LOGGER = logging.getLogger(__name__) 73 | 74 | @config_entries.HANDLERS.register(DOMAIN) 75 | class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 76 | @staticmethod 77 | @callback 78 | def async_get_options_flow(config_entry): 79 | """Get the options flow for this handler.""" 80 | return OptionsFlow(config_entry) 81 | 82 | def __init__(self): 83 | """Initialize.""" 84 | self._errors = {} 85 | self.session = requests.session() 86 | self.userid = None 87 | self.usertype = None 88 | self.cloudpgs_token = None 89 | 90 | def __encode(self, code): 91 | en_code = '' 92 | for s in code: 93 | en_code = en_code + str(ord(s)) + '|' 94 | return en_code[:-1] 95 | 96 | def md5_hash(self, text): 97 | md5 = hashlib.md5() 98 | md5.update(text.encode('utf-8')) 99 | encrypted_text = md5.hexdigest() 100 | return encrypted_text 101 | 102 | def _login_tuqiang123(self, username, password): 103 | p_data = { 104 | 'ver': '1', 105 | 'method': 'login', 106 | 'account': username, 107 | 'password': self.__encode(password), 108 | 'language': 'zh' 109 | } 110 | url = API_HOST_TUQIANG123 + '/api/regdc' 111 | response = self.session.post(url, data=p_data) 112 | verurl = API_HOST_TUQIANG123 + '/api/regdc?ver=1&method=getAuthWay&account=' + username 113 | resver = self.session.get(verurl) 114 | _LOGGER.debug(resver.json()) 115 | if not resver.json().get("data") == "": 116 | msg = "账号开户了" + resver.json().get("data") + "登录二次认证,请关闭二次验证后再尝试!" 117 | return {"msg":msg} 118 | _LOGGER.debug("headers: %s", self.session.headers) 119 | _LOGGER.debug("cookies: %s", self.session.cookies) 120 | _LOGGER.info(response.json()) 121 | if response.json()['code'] == 0: 122 | url = API_HOST_TUQIANG123 + '/customer/getProviderList' 123 | resp = self.session.post(url, data=None).json() 124 | _LOGGER.debug(resp) 125 | self.userid = resp['data']['user']['userId'] 126 | self.usertype = resp['data']['user']['type'] 127 | return response.json() 128 | 129 | 130 | def _devicelist_tuqiang123(self): 131 | url = API_HOST_TUQIANG123 + '/device/list' 132 | p_data = { 133 | 'dateType': 'activation', 134 | 'equipment.userId': self.userid 135 | } 136 | resp = self.session.post(url, data=p_data).json() 137 | return resp 138 | 139 | def _login_tuqiangnet(self, username, password): 140 | p_data = { 141 | 'timeZone': '28800', 142 | 'token': '', 143 | 'userName': username, 144 | 'password': password, 145 | 'lang': 'zh' 146 | } 147 | url = API_HOST_TUQIANGNET + '/loginVerification' 148 | response = self.session.post(url, data=p_data) 149 | _LOGGER.debug("headers: %s", self.session.headers) 150 | _LOGGER.debug("cookies: %s", self.session.cookies) 151 | _LOGGER.debug(response) 152 | if response.json()['code'] == 0: 153 | _LOGGER.info(response.json()) 154 | self.cloudpgs_token = response.json()["data"]["token"] 155 | return response.json() 156 | 157 | def _devicelist_tuqiangnet(self): 158 | url = API_HOST_TUQIANGNET + '/device/getDeviceList' 159 | p_data = { 160 | 'token': self.cloudpgs_token, 161 | 'userId': self.userid 162 | } 163 | resp = self.session.post(url, data=p_data).json() 164 | return resp 165 | 166 | def _login_gooddriver(self, username, password): 167 | p_data = { 168 | 'U_ACCOUNT': username, 169 | 'U_PASSWORD': self.md5_hash(password) 170 | } 171 | url = API_HOST_TOKEN_GOODDRIVER + '/UserServices/Login2018' 172 | response = self.session.post(url, data=json.dumps(p_data)) 173 | return response.json() 174 | 175 | def _devicelist_cmobd(self, token): 176 | url = API_URL_CMOBD 177 | p_data = { 178 | "cmd":"userVehicles", 179 | "ver":1, 180 | "token": token, 181 | "pageNo":0, 182 | "pageSize":10 183 | } 184 | resp = self.session.post(url, data=p_data).json() 185 | return resp 186 | 187 | def _get_cmobd_tracker(self, token, vehicleid): 188 | url = API_URL_CMOBD 189 | p_data = { 190 | "cmd": "weappVehicleRunStatus", 191 | "ver": 1, 192 | "token": token, 193 | "vehicleId": vehicleid, 194 | "isNeedGps": "1", 195 | "gpsStartTime": "" 196 | } 197 | resp = self.session.post(url, data=p_data).json() 198 | return resp 199 | 200 | def _get_niu_token(self, username, password): 201 | url = API_HOST_TOKEN_NIU + '/v3/api/oauth2/token' 202 | md5 = hashlib.md5(password.encode("utf-8")).hexdigest() 203 | data = { 204 | "account": username, 205 | "password": md5, 206 | "grant_type": "password", 207 | "scope": "base", 208 | "app_id": "niu_ktdrr960", 209 | } 210 | try: 211 | r = requests.post(url, data=data) 212 | except BaseException as e: 213 | print(e) 214 | return False 215 | data = json.loads(r.content.decode()) 216 | _LOGGER.debug("get niu token data: %s", data) 217 | return data 218 | 219 | def _get_niu_vehicles_info(self, token): 220 | 221 | url = API_URL_NIU + '/v5/scooter/list' 222 | headers = {"token": token} 223 | try: 224 | r = requests.get(url, headers=headers, data=[]) 225 | except ConnectionError: 226 | return False 227 | if r.status_code != 200: 228 | return False 229 | data = json.loads(r.content.decode()) 230 | return data 231 | 232 | def _devicelist_hellobike(self, token): 233 | url = API_URL_HELLOBIKE + "?rent.user.getUseBikePagePrimeInfoV3" 234 | p_data = { 235 | "token" : token, 236 | "action" : "rent.user.getUseBikePagePrimeInfoV3" 237 | } 238 | resp = self.session.post(url, data=json.dumps(p_data)).json() 239 | return resp 240 | 241 | def _get_hellobike_tracker(self, token, bikeNo): 242 | url = API_URL_HELLOBIKE + "?rent.order.getRentBikeStatus" 243 | p_data = { 244 | "bikeNo" : bikeNo, 245 | "token" : token, 246 | "action" : "rent.order.getRentBikeStatus" 247 | } 248 | resp = self.session.post(url, data=json.dumps(p_data)).json() 249 | return resp 250 | 251 | def _devicelist_autoamap(self, token): 252 | url = str.format(API_URL_AUTOAMAP + token.split("||")[0]) 253 | p_data = token.split("||")[2] 254 | resp = self.session.post(url, data=p_data).json() 255 | return resp 256 | 257 | async def async_step_user(self, user_input={}): 258 | self._errors = {} 259 | if user_input is not None: 260 | # Check if entered host is already in HomeAssistant 261 | existing = await self._check_existing(user_input[CONF_NAME]) 262 | if existing: 263 | return self.async_abort(reason="already_configured") 264 | 265 | # If it is not, continue with communication test 266 | config_data = {} 267 | username = user_input[CONF_USERNAME] 268 | password = user_input[CONF_PASSWORD] 269 | webhost = user_input[CONF_WEB_HOST] 270 | 271 | devices = [] 272 | 273 | if webhost=="tuqiang.net": 274 | headers = { 275 | 'User-Agent': USER_AGENT 276 | } 277 | self.session.headers = headers 278 | 279 | status = await self.hass.async_add_executor_job(self._login_tuqiangnet, username, password) 280 | if status.get("code") == 0: 281 | deviceslist_data = await self.hass.async_add_executor_job(self._devicelist_tuqiangnet) 282 | _LOGGER.debug(deviceslist_data) 283 | if deviceslist_data.get("code") == 0: 284 | for deviceslist in deviceslist_data["data"]: 285 | devices.append(str(deviceslist["imei"])) 286 | 287 | await self.async_set_unique_id(f"cloudpgs-{user_input[CONF_USERNAME]}-{user_input[CONF_WEB_HOST]}".replace(".","_")) 288 | self._abort_if_unique_id_configured() 289 | 290 | config_data[CONF_USERNAME] = username 291 | config_data[CONF_PASSWORD] = password 292 | config_data[CONF_DEVICES] = devices 293 | config_data[CONF_WEB_HOST] = webhost 294 | 295 | _LOGGER.debug(devices) 296 | 297 | return self.async_create_entry( 298 | title=user_input[CONF_NAME], data=config_data 299 | ) 300 | else: 301 | self._errors["base"] = status.get("msg") 302 | elif webhost=="tuqiang123.com": 303 | headers = { 304 | 'User-Agent': USER_AGENT 305 | } 306 | self.session.headers = headers 307 | 308 | status = await self.hass.async_add_executor_job(self._login_tuqiang123, username, password) 309 | if status.get("code") == 0: 310 | deviceslist_data = await self.hass.async_add_executor_job(self._devicelist_tuqiang123) 311 | _LOGGER.debug(deviceslist_data) 312 | if deviceslist_data.get("code") == 0: 313 | for deviceslist in deviceslist_data["data"]["result"]: 314 | devices.append(str(deviceslist["equipmentDetail"]["imei"])) 315 | 316 | await self.async_set_unique_id(f"cloudpgs-{user_input[CONF_USERNAME]}-{user_input[CONF_WEB_HOST]}".replace(".","_")) 317 | self._abort_if_unique_id_configured() 318 | 319 | config_data[CONF_USERNAME] = username 320 | config_data[CONF_PASSWORD] = password 321 | config_data[CONF_DEVICES] = devices 322 | config_data[CONF_WEB_HOST] = webhost 323 | 324 | _LOGGER.debug(devices) 325 | 326 | return self.async_create_entry( 327 | title=user_input[CONF_NAME], data=config_data 328 | ) 329 | else: 330 | self._errors["base"] = status.get("msg") 331 | 332 | elif webhost=="gooddriver.cn": 333 | headers = { 334 | 'User-Agent': USER_AGENT_GOODDRIVER, 335 | 'SDF': '6928FAA6-B970-F5A5-85F0-73D4299D99A8', 336 | 'Content-Type': 'application/x-www-form-urlencoded' 337 | } 338 | self.session.headers = headers 339 | 340 | self.session.verify = True 341 | status = await self.hass.async_add_executor_job(self._login_gooddriver, username, password) 342 | _LOGGER.debug(status) 343 | if status.get("ERROR_CODE") == 0: 344 | deviceslist_data = status["MESSAGE"]["USER_VEHICLEs"] 345 | _LOGGER.debug(deviceslist_data) 346 | for deviceslist in deviceslist_data: 347 | url = API_URL_GOODDRIVER + str(deviceslist["UV_ID"]) 348 | resp = await self.hass.async_add_executor_job(self.session.get, url) 349 | if resp.json()['ERROR_CODE'] == 0: 350 | devices.append(str(deviceslist["UV_ID"])) 351 | 352 | await self.async_set_unique_id(f"cloudpgs-{user_input[CONF_USERNAME]}-{user_input[CONF_WEB_HOST]}".replace(".","_")) 353 | self._abort_if_unique_id_configured() 354 | 355 | config_data[CONF_USERNAME] = username 356 | config_data[CONF_PASSWORD] = password 357 | config_data[CONF_DEVICES] = devices 358 | config_data[CONF_WEB_HOST] = webhost 359 | 360 | _LOGGER.debug(devices) 361 | 362 | return self.async_create_entry( 363 | title=user_input[CONF_NAME], data=config_data 364 | ) 365 | else: 366 | self._errors["base"] = status.get("ERROR_MESSAGE") 367 | 368 | elif webhost=="niu.com": 369 | headers = { 370 | 'User-Agent': USER_AGENT_NIU, 371 | 'Accept-Language': 'en-US' 372 | } 373 | self.session.headers = headers 374 | 375 | self.session.verify = True 376 | tokendata = await self.hass.async_add_executor_job(self._get_niu_token, username, password) 377 | if tokendata.get("status") != 0: 378 | self._errors["base"] = tokendata.get("desc") 379 | return await self._show_config_form(user_input) 380 | token = tokendata["data"]["token"]["access_token"] 381 | if token: 382 | devicelistinfo = await self.hass.async_add_executor_job(self._get_niu_vehicles_info, token) 383 | deviceslist_data = devicelistinfo["data"]["items"] 384 | _LOGGER.debug(deviceslist_data) 385 | for deviceslist in deviceslist_data: 386 | devices.append(str(deviceslist["sn_id"])) 387 | 388 | await self.async_set_unique_id(f"cloudpgs-{user_input[CONF_USERNAME]}-{user_input[CONF_WEB_HOST]}".replace(".","_")) 389 | self._abort_if_unique_id_configured() 390 | 391 | config_data[CONF_USERNAME] = username 392 | config_data[CONF_PASSWORD] = password 393 | config_data[CONF_DEVICES] = devices 394 | config_data[CONF_WEB_HOST] = webhost 395 | 396 | _LOGGER.debug(devices) 397 | 398 | return self.async_create_entry( 399 | title=user_input[CONF_NAME], data=config_data 400 | ) 401 | else: 402 | self._errors["base"] = "communication" 403 | elif webhost=="cmobd.com": 404 | headers = { 405 | 'Host': 'lsapp.cmobd.com', 406 | 'agent': 'Lushang/5.0.0', 407 | 'Cookie': 'node-ls-api=' + password, 408 | 'content-type': 'application/json', 409 | 'User-Agent': USER_AGENT_CMOBD, 410 | 'Referer': 'https://servicewechat.com/wx351871af12293380/31/page-frame.html' 411 | } 412 | 413 | self.session.headers = headers 414 | 415 | self.session.verify = True 416 | status = await self.hass.async_add_executor_job(self._devicelist_cmobd, password) 417 | _LOGGER.debug(status) 418 | if status.get("result") != 0: 419 | self._errors["base"] = status.get("note") 420 | return await self._show_config_form(user_input) 421 | if status: 422 | deviceslist_data = status.get("dataList") 423 | _LOGGER.debug(deviceslist_data) 424 | for deviceslist in deviceslist_data: 425 | resp = await self.hass.async_add_executor_job(self._get_cmobd_tracker, password, str(deviceslist["vehicleID"])) 426 | if resp['result'] == 0: 427 | devices.append(str(deviceslist["vehicleID"])) 428 | 429 | await self.async_set_unique_id(f"cloudpgs-{user_input[CONF_USERNAME]}-{user_input[CONF_WEB_HOST]}".replace(".","_")) 430 | self._abort_if_unique_id_configured() 431 | 432 | config_data[CONF_USERNAME] = username 433 | config_data[CONF_PASSWORD] = password 434 | config_data[CONF_DEVICES] = devices 435 | config_data[CONF_WEB_HOST] = webhost 436 | 437 | _LOGGER.debug(devices) 438 | 439 | return self.async_create_entry( 440 | title=user_input[CONF_NAME], data=config_data 441 | ) 442 | else: 443 | self._errors["base"] = "communication" 444 | elif webhost=="hellobike.com": 445 | headers = { 446 | 'content_type': 'text/plain;charset=utf-8', 447 | 'Accept': 'application/json, text/plain, */*' 448 | } 449 | self.session.headers = headers 450 | self.session.verify = True 451 | 452 | status = await self.hass.async_add_executor_job(self._devicelist_hellobike, password) 453 | _LOGGER.debug(status) 454 | 455 | if status.get("code") != 0: 456 | self._errors["base"] = status.get("msg") 457 | return await self._show_config_form(user_input) 458 | 459 | if status["data"].get("userBikeList"): 460 | deviceslist_data = status["data"]["userBikeList"] 461 | _LOGGER.debug(deviceslist_data) 462 | for deviceslist in deviceslist_data: 463 | resp = await self.hass.async_add_executor_job(self._get_hellobike_tracker, password, str(deviceslist["bikeNo"])) 464 | if resp['code'] == 0: 465 | devices.append(str(deviceslist["bikeNo"])) 466 | 467 | await self.async_set_unique_id(f"cloudpgs-{user_input[CONF_USERNAME]}-{user_input[CONF_WEB_HOST]}".replace(".","_")) 468 | self._abort_if_unique_id_configured() 469 | 470 | config_data[CONF_USERNAME] = username 471 | config_data[CONF_PASSWORD] = password 472 | config_data[CONF_DEVICES] = devices 473 | config_data[CONF_WEB_HOST] = webhost 474 | 475 | _LOGGER.debug(devices) 476 | 477 | return self.async_create_entry( 478 | title=user_input[CONF_NAME], data=config_data 479 | ) 480 | else: 481 | self._errors["base"] = "communication" 482 | elif webhost=="auto.amap.com": 483 | headers = { 484 | 'Host': 'ts.amap.com', 485 | 'Accept': 'application/json', 486 | 'sessionid': password.split("||")[1], 487 | 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', 488 | 'Cookie': 'sessionid=' + password.split("||")[1], 489 | } 490 | self.session.headers = headers 491 | self.session.verify = True 492 | 493 | status = await self.hass.async_add_executor_job(self._devicelist_autoamap, password) 494 | _LOGGER.debug(status) 495 | 496 | if status.get("result") != "true": 497 | self._errors["base"] = status.get("msg") 498 | return await self._show_config_form(user_input) 499 | 500 | if status["data"].get("carLinkInfoList"): 501 | deviceslist_data = status["data"]["carLinkInfoList"] 502 | _LOGGER.debug(deviceslist_data) 503 | for deviceslist in deviceslist_data: 504 | devices.append(str(deviceslist["tid"])) 505 | 506 | await self.async_set_unique_id(f"cloudpgs-{user_input[CONF_USERNAME]}-{user_input[CONF_WEB_HOST]}".replace(".","_")) 507 | self._abort_if_unique_id_configured() 508 | 509 | config_data[CONF_USERNAME] = username 510 | config_data[CONF_PASSWORD] = password 511 | config_data[CONF_DEVICES] = devices 512 | config_data[CONF_WEB_HOST] = webhost 513 | 514 | _LOGGER.debug(devices) 515 | 516 | return self.async_create_entry( 517 | title=user_input[CONF_NAME], data=config_data 518 | ) 519 | else: 520 | self._errors["base"] = "communication" 521 | else: 522 | self._errors["base"] = "未选择有效平台" 523 | 524 | return await self._show_config_form(user_input) 525 | 526 | return await self._show_config_form(user_input) 527 | 528 | async def _show_config_form(self, user_input): 529 | 530 | # Defaults 531 | device_name = "平台名称GPS" 532 | data_schema = OrderedDict() 533 | data_schema[vol.Required(CONF_NAME, default=device_name)] = str 534 | data_schema[vol.Required(CONF_USERNAME ,default ="")] = str 535 | data_schema[vol.Required(CONF_PASSWORD ,default ="")] = str 536 | data_schema[vol.Required(CONF_WEB_HOST, default="")] = vol.All(str, vol.In(WEBHOST)) 537 | 538 | return self.async_show_form( 539 | step_id="user", data_schema=vol.Schema(data_schema), errors=self._errors 540 | ) 541 | 542 | async def _check_existing(self, host): 543 | for entry in self._async_current_entries(): 544 | if host == entry.data.get(CONF_NAME): 545 | return True 546 | 547 | class OptionsFlow(config_entries.OptionsFlow): 548 | """Config flow options for cloud.""" 549 | 550 | def __init__(self, config_entry): 551 | """Initialize cloud options flow.""" 552 | #self._config_entry = config_entry 553 | self._conf_app_id: str | None = None 554 | self._config = dict(config_entry.data) 555 | 556 | async def async_step_init(self, user_input=None): 557 | """Manage the options.""" 558 | return await self.async_step_user() 559 | 560 | def update_password_from_user_input(self, entry_password: str | None, user_input: dict[str, any]) -> dict[str, any]: 561 | """Update the password if the entry has been updated. 562 | 563 | As we want to avoid reflecting the stored password in the UI, 564 | we replace the suggested value in the UI with a sentinel, 565 | and we change it back here if it was changed. 566 | """ 567 | substituted_used_data = dict(user_input) 568 | # Take out the password submitted 569 | user_password: str | None = substituted_used_data.pop(CONF_PASSWORD, None) 570 | # Only add the password if it has changed. 571 | # If the sentinel password is submitted, we replace that with our current 572 | # password from the config entry data. 573 | password_changed = user_password is not None and user_password != PWD_NOT_CHANGED 574 | password = user_password if password_changed else entry_password 575 | substituted_used_data[CONF_PASSWORD] = password 576 | return substituted_used_data 577 | 578 | async def async_step_user(self, user_input=None): 579 | """Handle a flow initialized by the user.""" 580 | if user_input is not None: 581 | updated_user_input = self.update_password_from_user_input(self._config.get("password"), user_input) 582 | self._config.update(updated_user_input) 583 | self.hass.config_entries.async_update_entry( 584 | self.config_entry, 585 | data=self._config 586 | ) 587 | await self.hass.config_entries.async_reload(self._config_entry_id) 588 | return self.async_create_entry(title="", data=self._config) 589 | 590 | listoptions = [] 591 | for deviceconfig in self.config_entry.data.get(CONF_DEVICES,[]): 592 | listoptions.append({"value": deviceconfig, "label": deviceconfig}) 593 | 594 | if self.config_entry.data.get(CONF_WEB_HOST) == "tuqiang123.com": 595 | SENSORSLIST = [ 596 | {"value": KEY_PARKING_TIME, "label": "parkingtime"}, 597 | {"value": KEY_LASTSTOPTIME, "label": "laststoptime"}, 598 | {"value": KEY_ADDRESS, "label": "address"}, 599 | {"value": KEY_SPEED, "label": "speed"}, 600 | {"value": KEY_TOTALKM, "label": "totalkm"}, 601 | {"value": KEY_STATUS, "label": "status"}, 602 | {"value": KEY_ACC, "label": "acc"}, 603 | {"value": KEY_BATTERY, "label": "powbattery"} 604 | ] 605 | SWITCHSLIST = [] 606 | BUTTONSLIST = [ 607 | {"value": "nowtrack", "label": "nowtrack"} 608 | ] 609 | elif self.config_entry.data.get(CONF_WEB_HOST) == "hellobike.com": 610 | SENSORSLIST = [ 611 | {"value": KEY_PARKING_TIME, "label": "parkingtime"}, 612 | {"value": KEY_LASTSTOPTIME, "label": "laststoptime"}, 613 | {"value": KEY_ADDRESS, "label": "address"}, 614 | {"value": KEY_STATUS, "label": "status"}, 615 | {"value": KEY_ACC, "label": "acc"}, 616 | {"value": KEY_BATTERY, "label": "powbattery"} 617 | ] 618 | 619 | SWITCHSLIST = [ 620 | {"value": "defence", "label": "defence"}, 621 | {"value": "open_lock", "label": "open_lock"}, 622 | ] 623 | 624 | BUTTONSLIST = [ 625 | {"value": "bell", "label": "bell"} 626 | ] 627 | elif self.config_entry.data.get(CONF_WEB_HOST) == "gooddriver.cn": 628 | SENSORSLIST = [ 629 | {"value": KEY_PARKING_TIME, "label": "parkingtime"}, 630 | {"value": KEY_LASTSTOPTIME, "label": "laststoptime"}, 631 | {"value": KEY_ADDRESS, "label": "address"}, 632 | {"value": KEY_SPEED, "label": "speed"}, 633 | {"value": KEY_STATUS, "label": "status"}, 634 | {"value": KEY_TOTALKM, "label": "totalkm"}, 635 | {"value": KEY_ACC, "label": "acc"} 636 | ] 637 | 638 | SWITCHSLIST = [] 639 | BUTTONSLIST = [] 640 | elif self.config_entry.data.get(CONF_WEB_HOST) == "cmobd.com": 641 | SENSORSLIST = [ 642 | {"value": KEY_PARKING_TIME, "label": "parkingtime"}, 643 | {"value": KEY_LASTSTOPTIME, "label": "laststoptime"}, 644 | {"value": KEY_ADDRESS, "label": "address"}, 645 | {"value": KEY_SPEED, "label": "speed"}, 646 | {"value": KEY_STATUS, "label": "status"}, 647 | {"value": KEY_ACC, "label": "acc"} 648 | ] 649 | 650 | SWITCHSLIST = [] 651 | BUTTONSLIST = [] 652 | elif self.config_entry.data.get(CONF_WEB_HOST) == "auto.amap.com": 653 | SENSORSLIST = [ 654 | {"value": KEY_PARKING_TIME, "label": "parkingtime"}, 655 | {"value": KEY_LASTSTOPTIME, "label": "laststoptime"}, 656 | {"value": KEY_SPEED, "label": "speed"}, 657 | {"value": KEY_ADDRESS, "label": "address"}, 658 | {"value": KEY_STATUS, "label": "status"}, 659 | ] 660 | 661 | SWITCHSLIST = [] 662 | BUTTONSLIST = [] 663 | else: 664 | SENSORSLIST = [ 665 | {"value": KEY_PARKING_TIME, "label": "parkingtime"}, 666 | {"value": KEY_LASTSTOPTIME, "label": "laststoptime"}, 667 | {"value": KEY_ADDRESS, "label": "address"}, 668 | {"value": KEY_SPEED, "label": "speed"}, 669 | {"value": KEY_TOTALKM, "label": "totalkm"}, 670 | {"value": KEY_STATUS, "label": "status"}, 671 | {"value": KEY_ACC, "label": "acc"}, 672 | {"value": KEY_BATTERY, "label": "powbattery"} 673 | ] 674 | SWITCHSLIST = [] 675 | BUTTONSLIST = [] 676 | 677 | return self.async_show_form( 678 | step_id="user", 679 | data_schema=vol.Schema( 680 | { 681 | vol.Required(CONF_PASSWORD, default=PWD_NOT_CHANGED): cv.string, 682 | vol.Optional( 683 | CONF_DEVICE_IMEI, 684 | default=self.config_entry.options.get(CONF_DEVICE_IMEI,[])): SelectSelector( 685 | SelectSelectorConfig( 686 | options=listoptions, 687 | multiple=True,translation_key=CONF_DEVICE_IMEI 688 | ) 689 | ), 690 | vol.Optional( 691 | CONF_UPDATE_INTERVAL, 692 | default=self.config_entry.options.get(CONF_UPDATE_INTERVAL, 60), 693 | ): vol.All(vol.Coerce(int), vol.Range(min=10, max=3600)), 694 | vol.Optional( 695 | CONF_GPS_CONVER, 696 | default=self.config_entry.options.get(CONF_GPS_CONVER,"wgs84") 697 | ): SelectSelector( 698 | SelectSelectorConfig( 699 | options=[ 700 | {"value": "wgs84", "label": "wgs84"}, 701 | {"value": "gcj02", "label": "gcj02"}, 702 | {"value": "bd09", "label": "bd09"} 703 | ], 704 | multiple=False,translation_key=CONF_GPS_CONVER 705 | ) 706 | ), 707 | vol.Optional( 708 | CONF_ATTR_SHOW, 709 | default=self.config_entry.options.get(CONF_ATTR_SHOW, True), 710 | ): bool, 711 | vol.Optional( 712 | CONF_WITH_MAP_CARD, 713 | default=self.config_entry.options.get(CONF_WITH_MAP_CARD,"none") 714 | ): SelectSelector( 715 | SelectSelectorConfig( 716 | options=[ 717 | {"value": "none", "label": "none"}, 718 | {"value": "baidu-map", "label": "baidu-map"}, 719 | {"value": "gaode-map", "label": "gaode-map"}, 720 | ], 721 | multiple=False,translation_key=CONF_WITH_MAP_CARD 722 | ) 723 | ), 724 | vol.Optional( 725 | CONF_SENSORS, 726 | default=self.config_entry.options.get(CONF_SENSORS,[]) 727 | ): SelectSelector( 728 | SelectSelectorConfig( 729 | options=SENSORSLIST, 730 | multiple=True,translation_key=CONF_SENSORS 731 | ) 732 | ), 733 | vol.Optional( 734 | CONF_SWITCHS, 735 | default=self.config_entry.options.get(CONF_SWITCHS,[]) 736 | ): SelectSelector( 737 | SelectSelectorConfig( 738 | options=SWITCHSLIST, 739 | multiple=True,translation_key=CONF_SWITCHS 740 | ) 741 | ), 742 | vol.Optional( 743 | CONF_BUTTONS, 744 | default=self.config_entry.options.get(CONF_BUTTONS,[]) 745 | ): SelectSelector( 746 | SelectSelectorConfig( 747 | options=BUTTONSLIST, 748 | multiple=True,translation_key=CONF_BUTTONS 749 | ) 750 | ), 751 | vol.Optional( 752 | CONF_UPDATE_ADDRESSDISTANCE, 753 | default=self.config_entry.options.get(CONF_UPDATE_ADDRESSDISTANCE, 50), 754 | ): vol.All(vol.Coerce(int), vol.Range(min=10, max=10000)), 755 | vol.Optional( 756 | CONF_ADDRESSAPI, 757 | default=self.config_entry.options.get(CONF_ADDRESSAPI,"none") 758 | ): SelectSelector( 759 | SelectSelectorConfig( 760 | options=[ 761 | {"value": "none", "label": "none"}, 762 | {"value": "free", "label": "free"}, 763 | {"value": "gaode", "label": "gaode"}, 764 | {"value": "baidu", "label": "baidu"}, 765 | {"value": "tencent", "label": "tencent"} 766 | ], 767 | multiple=False,translation_key=CONF_ADDRESSAPI 768 | ) 769 | ), 770 | vol.Optional( 771 | CONF_ADDRESSAPI_KEY, 772 | default=self.config_entry.options.get(CONF_ADDRESSAPI_KEY,"") 773 | ): str, 774 | vol.Optional( 775 | CONF_PRIVATE_KEY, 776 | default=self.config_entry.options.get(CONF_PRIVATE_KEY,"") 777 | ): str, 778 | } 779 | ), 780 | ) 781 | 782 | -------------------------------------------------------------------------------- /custom_components/cloud_gps/const.py: -------------------------------------------------------------------------------- 1 | 2 | """Constants for cloud gps.""" 3 | DOMAIN = "cloud_gps" 4 | 5 | REQUIRED_FILES = [ 6 | "const.py", 7 | "manifest.json", 8 | "device_tracker.py", 9 | "config_flow.py", 10 | "translations/en.json", 11 | "translations/zh-Hans.json", 12 | ] 13 | VERSION = "2025.1.29" 14 | ISSUE_URL = "https://github.com/dscao/cloud_gps/issues" 15 | 16 | STARTUP = """ 17 | ------------------------------------------------------------------- 18 | {name} 19 | Version: {version} 20 | This is a custom component 21 | If you have any issues with this you need to open an issue here: 22 | {issueurl} 23 | ------------------------------------------------------------------- 24 | """ 25 | 26 | from homeassistant.const import ( 27 | ATTR_DEVICE_CLASS, 28 | ) 29 | 30 | ATTR_ICON = "icon" 31 | ATTR_LABEL = "label" 32 | MANUFACTURER = "云平台" 33 | NAME = "云平台GPS" 34 | 35 | CONF_WEB_HOST = "webhost" 36 | 37 | CONF_DEVICES = "devices" 38 | CONF_DEVICE_IMEI = "device_imei" 39 | CONF_GPS_CONVER = "gps_conver" 40 | CONF_ATTR_SHOW = "attr_show" 41 | CONF_UPDATE_INTERVAL = "update_interval_seconds" 42 | CONF_SENSORS = "sensors" 43 | CONF_SWITCHS = "switchs" 44 | CONF_BUTTONS = "buttons" 45 | CONF_MAP_GCJ_LAT = "map_gcj_lat" 46 | CONF_MAP_GCJ_LNG = "map_gcj_lng" 47 | CONF_MAP_BD_LAT = "map_bd_lat" 48 | CONF_MAP_BD_LNG = "map_bd_lng" 49 | CONF_UPDATE_ADDRESSDISTANCE = "address_distance" 50 | CONF_ADDRESSAPI = "addressapi" 51 | CONF_ADDRESSAPI_KEY = "api_key" 52 | CONF_PRIVATE_KEY = "private_key" 53 | CONF_WITH_MAP_CARD = "with_map_card" 54 | 55 | COORDINATOR = "coordinator" 56 | UNDO_UPDATE_LISTENER = "undo_update_listener" 57 | 58 | PWD_NOT_CHANGED = "__**password_not_changed**__" 59 | 60 | KEY_ADDRESS = "address" 61 | KEY_QUERYTIME = "querytime" 62 | KEY_PARKING_TIME = "parkingtime" 63 | KEY_LASTSTOPTIME = "laststoptime" 64 | KEY_SPEED = "speed" 65 | KEY_TOTALKM = "totalkm" 66 | KEY_STATUS = "status" 67 | KEY_ACC = "acc" 68 | KEY_BATTERY = "powbattery" 69 | 70 | 71 | -------------------------------------------------------------------------------- /custom_components/cloud_gps/device_tracker.py: -------------------------------------------------------------------------------- 1 | """Support for the cloud_gps service.""" 2 | import logging 3 | import time, datetime 4 | import requests 5 | import re 6 | import json 7 | import hashlib 8 | import urllib.parse 9 | 10 | from aiohttp.client_exceptions import ClientConnectorError 11 | from homeassistant.components.device_tracker.config_entry import TrackerEntity 12 | from homeassistant.helpers.device_registry import DeviceEntryType 13 | 14 | from .helper import gcj02towgs84, wgs84togcj02, gcj02_to_bd09 15 | 16 | from homeassistant.const import ( 17 | CONF_NAME, 18 | CONF_USERNAME, 19 | CONF_PASSWORD, 20 | CONF_CLIENT_ID, 21 | ATTR_GPS_ACCURACY, 22 | ATTR_LATITUDE, 23 | ATTR_LONGITUDE, 24 | STATE_HOME, 25 | STATE_NOT_HOME, 26 | MAJOR_VERSION, 27 | MINOR_VERSION, 28 | ) 29 | 30 | from .const import ( 31 | COORDINATOR, 32 | DOMAIN, 33 | CONF_WEB_HOST, 34 | UNDO_UPDATE_LISTENER, 35 | CONF_ATTR_SHOW, 36 | MANUFACTURER, 37 | CONF_PRIVATE_KEY, 38 | CONF_MAP_GCJ_LAT, 39 | CONF_MAP_GCJ_LNG, 40 | CONF_MAP_BD_LAT, 41 | CONF_MAP_BD_LNG, 42 | CONF_WITH_MAP_CARD, 43 | ) 44 | 45 | PARALLEL_UPDATES = 1 46 | _LOGGER = logging.getLogger(__name__) 47 | 48 | async def async_setup_entry(hass, config_entry, async_add_entities): 49 | """Add cloud entities from a config_entry.""" 50 | webhost = config_entry.data[CONF_WEB_HOST] 51 | attr_show = config_entry.options.get(CONF_ATTR_SHOW, True) 52 | with_map_card = config_entry.options.get(CONF_WITH_MAP_CARD, "none") 53 | coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] 54 | 55 | for coordinatordata in coordinator.data: 56 | _LOGGER.debug("coordinatordata") 57 | _LOGGER.debug(coordinatordata) 58 | async_add_entities([CloudGPSEntity(hass, webhost, coordinatordata, attr_show, with_map_card, coordinator)], False) 59 | 60 | 61 | class CloudGPSEntity(TrackerEntity): 62 | """Representation of a tracker condition.""" 63 | _attr_has_entity_name = True 64 | _attr_name = None 65 | _attr_translation_key = "cloud_device_tracker" 66 | def __init__(self, hass, webhost, imei, attr_show, with_map_card, coordinator): 67 | self._hass = hass 68 | self._imei = imei 69 | self._webhost = webhost 70 | self.coordinator = coordinator 71 | self._attr_show = attr_show 72 | self._with_map_card = with_map_card 73 | self._attrs = {} 74 | self._coords = [self.coordinator.data[self._imei]["thislon"], self.coordinator.data[self._imei]["thislat"]] 75 | 76 | @property 77 | def unique_id(self): 78 | """Return a unique_id for this entity.""" 79 | _LOGGER.debug("device_tracker_unique_id: %s", self.coordinator.data[self._imei]["location_key"]) 80 | return self.coordinator.data[self._imei]["location_key"] 81 | 82 | @property 83 | def device_info(self): 84 | """Return the device info.""" 85 | return { 86 | "identifiers": {(DOMAIN, self.coordinator.data[self._imei]["location_key"])}, 87 | "name": self._imei, 88 | "manufacturer": self._webhost, 89 | "entry_type": DeviceEntryType.SERVICE, 90 | "model": self.coordinator.data[self._imei]["deviceinfo"]["device_model"], 91 | "sw_version": self.coordinator.data[self._imei]["deviceinfo"]["sw_version"], 92 | } 93 | @property 94 | def should_poll(self): 95 | """Return the polling requirement of the entity.""" 96 | return True 97 | 98 | # @property 99 | # def available(self): 100 | # """Return True if entity is available.""" 101 | # return self.trackerdata.last_update_success 102 | 103 | @property 104 | def icon(self): 105 | """Return the icon.""" 106 | return "mdi:car" 107 | 108 | @property 109 | def source_type(self): 110 | return "gps" 111 | 112 | @property 113 | def latitude(self): 114 | return self._coords[1] 115 | 116 | @property 117 | def longitude(self): 118 | return self._coords[0] 119 | 120 | @property 121 | def location_accuracy(self): 122 | return 0 123 | 124 | @property 125 | def state_attributes(self): 126 | attrs = super(CloudGPSEntity, self).state_attributes 127 | #data = self.trackerdata.get("result") 128 | if self.coordinator.data[self._imei]: 129 | attrs["status"] = self.coordinator.data[self._imei]["status"] 130 | if attrs.get("imei"): 131 | attrs["imei"] = self.coordinator.data[self._imei]["imei"] 132 | if self._with_map_card != "none" and self._with_map_card != None: 133 | attrs["custom_ui_more_info"] = self._with_map_card 134 | if self._attr_show == True: 135 | attrslist = self.coordinator.data[self._imei]["attrs"] 136 | for key, value in attrslist.items(): 137 | attrs[key] = value 138 | if self.coordinator.data[self._imei]["deviceinfo"].get("expiration"): 139 | attrs["expiration"] = self.coordinator.data[self._imei]["deviceinfo"]["expiration"] 140 | 141 | gcjdata = wgs84togcj02(self.coordinator.data[self._imei]["thislon"], self.coordinator.data[self._imei]["thislat"]) 142 | attrs[CONF_MAP_GCJ_LAT] = gcjdata[1] 143 | attrs[CONF_MAP_GCJ_LNG] = gcjdata[0] 144 | bddata = gcj02_to_bd09(gcjdata[0], gcjdata[1]) 145 | attrs[CONF_MAP_BD_LAT] = bddata[1] 146 | attrs[CONF_MAP_BD_LNG] = bddata[0] 147 | return attrs 148 | 149 | 150 | async def async_added_to_hass(self): 151 | """Connect to dispatcher listening for entity data notifications.""" 152 | self.async_on_remove( 153 | self.coordinator.async_add_listener(self.async_write_ha_state) 154 | ) 155 | 156 | async def async_update(self): 157 | """Update cloud entity.""" 158 | _LOGGER.debug("刷新device_tracker数据: %s %s", datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo, datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") ) 159 | #await self.coordinator.async_request_refresh() 160 | if self.coordinator.data.get(self._imei): 161 | self._coords = [self.coordinator.data[self._imei]["thislon"], self.coordinator.data[self._imei]["thislat"]] 162 | 163 | -------------------------------------------------------------------------------- /custom_components/cloud_gps/gooddriver_data_fetcher.py: -------------------------------------------------------------------------------- 1 | """ 2 | get info 3 | """ 4 | 5 | import logging 6 | import requests 7 | import re 8 | import asyncio 9 | import json 10 | import time 11 | import datetime 12 | import hashlib 13 | from async_timeout import timeout 14 | from aiohttp.client_exceptions import ClientConnectorError 15 | from homeassistant.helpers.aiohttp_client import async_create_clientsession 16 | from homeassistant.helpers.update_coordinator import UpdateFailed 17 | from urllib3.util.retry import Retry 18 | from requests.adapters import HTTPAdapter 19 | from homeassistant.const import ( 20 | CONF_USERNAME, 21 | CONF_PASSWORD, 22 | CONF_CLIENT_ID, 23 | ) 24 | 25 | from .const import ( 26 | COORDINATOR, 27 | DOMAIN, 28 | CONF_WEB_HOST, 29 | CONF_DEVICE_IMEI, 30 | UNDO_UPDATE_LISTENER, 31 | CONF_ATTR_SHOW, 32 | CONF_UPDATE_INTERVAL, 33 | ) 34 | 35 | _LOGGER = logging.getLogger(__name__) 36 | 37 | USER_AGENT = 'gooddriver/7.9.1 CFNetwork/1410.0.3 Darwin/22.6.0' 38 | GOODDRIVER_API_HOST_TOKEN = "https://ssl.gooddriver.cn" 39 | GOODDRIVER_API_TRACKER_URL = "http://restcore.gooddriver.cn/API/Values/HudDeviceDetail/" 40 | 41 | class DataFetcher: 42 | """fetch the cloud gps data""" 43 | 44 | def __init__(self, hass, username, password, device_imei, location_key): 45 | self.hass = hass 46 | self.location_key = location_key 47 | self.username = username 48 | self.password = password 49 | self.device_imei = device_imei 50 | self.session_gooddriver = requests.session() 51 | self.cloudpgs_token = None 52 | self.u_id = None 53 | self._lat_old = 0 54 | self._lon_old = 0 55 | self.deviceinfo = {} 56 | self.trackerdata = {} 57 | self.address = {} 58 | self.totalkm = {} 59 | 60 | headers = { 61 | 'User-Agent': USER_AGENT, 62 | 'SDF': '6928FAA6-B970-F5A5-85F0-73D4299D99A8', 63 | 'Content-Type': 'application/x-www-form-urlencoded' 64 | } 65 | self.session_gooddriver.headers.update(headers) 66 | 67 | def _is_json(self, jsonstr): 68 | try: 69 | json.loads(jsonstr) 70 | except ValueError: 71 | return False 72 | return True 73 | 74 | def md5_hash(self, text): 75 | md5 = hashlib.md5() 76 | md5.update(text.encode('utf-8')) 77 | encrypted_text = md5.hexdigest() 78 | return encrypted_text 79 | 80 | def _login(self, username, password): 81 | p_data = { 82 | 'U_ACCOUNT': username, 83 | 'U_PASSWORD': self.md5_hash(password) 84 | } 85 | url = GOODDRIVER_API_HOST_TOKEN + '/UserServices/Login2018' 86 | response = self.session_gooddriver.post(url, data=json.dumps(p_data)) 87 | if response.json()['ERROR_CODE'] == 0: 88 | #self.cloudpgs_token = response.json()["MESSAGE"]["U_ACCESS_TOKEN"] 89 | return response.json()["MESSAGE"] 90 | else: 91 | _LOGGER.error(response.json()) 92 | return None 93 | 94 | def _get_device_tracker(self, uv_id): 95 | url = GOODDRIVER_API_TRACKER_URL + str(uv_id) 96 | resp = self.session_gooddriver.get(url) 97 | return resp.json()['MESSAGE'] 98 | 99 | def time_diff(self, timestamp): 100 | result = datetime.datetime.now() - datetime.datetime.fromtimestamp(timestamp) 101 | hours = int(result.seconds / 3600) 102 | minutes = int(result.seconds % 3600 / 60) 103 | seconds = result.seconds%3600%60 104 | if result.days > 0: 105 | return("{0}天{1}小时{2}分钟".format(result.days,hours,minutes)) 106 | elif hours > 0: 107 | return("{0}小时{1}分钟".format(hours,minutes)) 108 | elif minutes > 0: 109 | return("{0}分钟{1}秒".format(minutes,seconds)) 110 | else: 111 | return("{0}秒".format(seconds)) 112 | 113 | async def get_data(self): 114 | 115 | if self.u_id is None: 116 | deviceslistinfo = await self.hass.async_add_executor_job(self._login, self.username, self.password) 117 | _LOGGER.debug("deviceslistinfo: %s", deviceslistinfo) 118 | for deviceinfo in deviceslistinfo["USER_VEHICLEs"]: 119 | self.deviceinfo[str(deviceinfo["UV_ID"])] = {} 120 | for deviceinfo in deviceslistinfo["USER_VEHICLEs"]: 121 | self.deviceinfo[str(deviceinfo["UV_ID"])]["device_model"] = deviceinfo["DEVICE"]["P_MODEL"] 122 | self.deviceinfo[str(deviceinfo["UV_ID"])]["sw_version"] = deviceinfo["DEVICE"]["D_ATI_VERSION"] 123 | self.deviceinfo[str(deviceinfo["UV_ID"])]["expiration"] = "永久" 124 | self.totalkm[str(deviceinfo["UV_ID"])] = deviceinfo["UV_CURRENT_MILEAGE"] 125 | 126 | 127 | for imei in self.device_imei: 128 | _LOGGER.debug("Requests imei: %s", imei) 129 | self.trackerdata[imei] = {} 130 | 131 | try: 132 | async with timeout(10): 133 | data = await self.hass.async_add_executor_job(self._get_device_tracker, imei) 134 | except ClientConnectorError as error: 135 | _LOGGER.error("连接错误: %s", error) 136 | except asyncio.TimeoutError: 137 | _LOGGER.error("获取数据超时 (10秒)") 138 | except Exception as e: 139 | _LOGGER.error("未知错误: %s", repr(e)) 140 | finally: 141 | _LOGGER.debug("最终数据结果: %s", data) 142 | 143 | if data: 144 | querytime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 145 | updatetime = data["HD_STATE_TIME"] 146 | imei = str(data["UV_ID"]) 147 | recent_location = json.loads(data["HD_RECENT_LOCATION"]) 148 | course = recent_location["Course"] 149 | speed = float(recent_location["Speed"]) 150 | _LOGGER.debug("speed: %s", speed) 151 | 152 | status = "停车" 153 | 154 | if data["HD_STATE"] == 1: 155 | acc = "车辆点火" 156 | status = "钥匙开启" 157 | elif data["HD_STATE"] == 2: 158 | acc = "车辆熄火" 159 | else: 160 | acc = "未知" 161 | 162 | thislat = float(recent_location["Lat"]) 163 | thislon = float(recent_location["Lng"]) 164 | laststoptime = recent_location["Time"] 165 | 166 | positionType = "GPS" 167 | if speed == 0: 168 | runorstop = "静止" 169 | parkingtime = self.time_diff(int(time.mktime(time.strptime(laststoptime, "%Y-%m-%d %H:%M:%S")))) 170 | else: 171 | runorstop = "运动" 172 | parkingtime = "" 173 | status = "行驶" 174 | 175 | totalKm = self.totalkm[imei] 176 | 177 | attrs = { 178 | "speed":speed, 179 | "course":course, 180 | "querytime":querytime, 181 | "laststoptime":laststoptime, 182 | "last_update":updatetime, 183 | "runorstop":runorstop, 184 | "acc":acc, 185 | "parkingtime":parkingtime, 186 | "totalKm":totalKm 187 | } 188 | 189 | self.trackerdata[imei] = {"location_key":self.location_key+str(imei),"deviceinfo":self.deviceinfo[imei],"thislat":thislat,"thislon":thislon,"status":status,"attrs":attrs} 190 | 191 | return self.trackerdata 192 | 193 | 194 | class GetDataError(Exception): 195 | """request error or response data is unexpected""" 196 | -------------------------------------------------------------------------------- /custom_components/cloud_gps/hellobike_data_fetcher.py: -------------------------------------------------------------------------------- 1 | """ 2 | get info 3 | """ 4 | 5 | import logging 6 | import requests 7 | import re 8 | import asyncio 9 | import json 10 | import time 11 | import datetime 12 | import hashlib 13 | from async_timeout import timeout 14 | from aiohttp.client_exceptions import ClientConnectorError 15 | from homeassistant.helpers.aiohttp_client import async_create_clientsession 16 | from homeassistant.helpers.update_coordinator import UpdateFailed 17 | from urllib3.util.retry import Retry 18 | from requests.adapters import HTTPAdapter 19 | from homeassistant.const import ( 20 | CONF_USERNAME, 21 | CONF_PASSWORD, 22 | CONF_CLIENT_ID, 23 | ) 24 | 25 | from .const import ( 26 | COORDINATOR, 27 | DOMAIN, 28 | CONF_WEB_HOST, 29 | CONF_DEVICE_IMEI, 30 | UNDO_UPDATE_LISTENER, 31 | CONF_ATTR_SHOW, 32 | CONF_UPDATE_INTERVAL, 33 | ) 34 | 35 | _LOGGER = logging.getLogger(__name__) 36 | 37 | HELLOBIKE_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.43(0x18002b2d) NetType/4G Language/zh_CN' 38 | HELLOBIKE_API_URL = "https://a.hellobike.com/evehicle/api" 39 | 40 | class DataFetcher: 41 | """fetch the cloud gps data""" 42 | 43 | def __init__(self, hass, username, password, device_imei, location_key): 44 | self.hass = hass 45 | self.location_key = location_key 46 | self._username = username 47 | self._password = password 48 | self.device_imei = device_imei 49 | self.session_hellobike = requests.session() 50 | self.cloudpgs_token = None 51 | self._lat_old = 0 52 | self._lon_old = 0 53 | self.deviceinfo = {} 54 | self.trackerdata = {} 55 | self.address = {} 56 | self.totalkm = {} 57 | 58 | headers = { 59 | 'content-type': 'application/json; charset=utf-8', 60 | 'User-Agent': HELLOBIKE_USER_AGENT 61 | } 62 | self.session_hellobike.headers.update(headers) 63 | 64 | def _is_json(self, jsonstr): 65 | try: 66 | json.loads(jsonstr) 67 | except ValueError: 68 | return False 69 | return True 70 | 71 | def md5_hash(self, text): 72 | md5 = hashlib.md5() 73 | md5.update(text.encode('utf-8')) 74 | encrypted_text = md5.hexdigest() 75 | return encrypted_text 76 | 77 | def _devicelist_hellobike(self, token): 78 | url = HELLOBIKE_API_URL + "?rent.user.getUseBikePagePrimeInfoV3" 79 | p_data = { 80 | "token" : token, 81 | "action" : "rent.user.getUseBikePagePrimeInfoV3" 82 | } 83 | resp = self.session_hellobike.post(url, data=json.dumps(p_data)).json() 84 | return resp 85 | 86 | def _get_device_tracker_hellobike(self, token, bikeNo): 87 | url = HELLOBIKE_API_URL + '?rent.order.getRentBikeStatus' 88 | p_data = {"bikeNo" : bikeNo,"token" : token,"action" : "rent.order.getRentBikeStatus"} 89 | resp = self.session_hellobike.post(url, data=json.dumps(p_data)).json() 90 | return resp 91 | 92 | def time_diff(self, timestamp): 93 | result = datetime.datetime.now() - datetime.datetime.fromtimestamp(timestamp) 94 | hours = int(result.seconds / 3600) 95 | minutes = int(result.seconds % 3600 / 60) 96 | seconds = result.seconds%3600%60 97 | if result.days > 0: 98 | return("{0}天{1}小时{2}分钟".format(result.days,hours,minutes)) 99 | elif hours > 0: 100 | return("{0}小时{1}分钟".format(hours,minutes)) 101 | elif minutes > 0: 102 | return("{0}分钟{1}秒".format(minutes,seconds)) 103 | else: 104 | return("{0}秒".format(seconds)) 105 | 106 | async def get_data(self): 107 | 108 | if self.deviceinfo == {}: 109 | deviceslistinfo = await self.hass.async_add_executor_job(self._devicelist_hellobike, self._password) 110 | _LOGGER.debug("deviceslistinfo: %s", deviceslistinfo) 111 | if deviceslistinfo.get("code") != 0: 112 | _LOGGER.error("请求api错误: %s", deviceslistinfo.get("msg")) 113 | return 114 | for deviceinfo in deviceslistinfo["data"].get("userBikeList"): 115 | self.deviceinfo[str(deviceinfo["bikeNo"])] = {} 116 | for deviceinfo in deviceslistinfo["data"].get("userBikeList"): 117 | self.deviceinfo[str(deviceinfo["bikeNo"])]["device_model"] = deviceinfo["modelName"] 118 | self.deviceinfo[str(deviceinfo["bikeNo"])]["sw_version"] = deviceinfo["tboxType"] + str(deviceinfo["pageVersionCode"]) +"." + str(deviceinfo["projectVersion"]) 119 | self.deviceinfo[str(deviceinfo["bikeNo"])]["expiration"] = "" 120 | 121 | 122 | for imei in self.device_imei: 123 | _LOGGER.debug("Requests bikeNo: %s", imei) 124 | 125 | self.trackerdata[imei] = {} 126 | 127 | try: 128 | async with timeout(10): 129 | data = await self.hass.async_add_executor_job(self._get_device_tracker_hellobike, self._password, imei) 130 | except ClientConnectorError as error: 131 | _LOGGER.error("连接错误: %s", error) 132 | except asyncio.TimeoutError: 133 | _LOGGER.error("获取数据超时 (10秒)") 134 | except Exception as e: 135 | _LOGGER.error("未知错误: %s", repr(e)) 136 | finally: 137 | _LOGGER.debug("最终数据结果: %s", data) 138 | 139 | if data: 140 | defenceStatus = data["data"]["defenceStatus"] 141 | cusionSensorState = data["data"]["cusionSensorState"] 142 | mainBatteryEletric = data["data"]["mainBatteryEletric"] 143 | simRssi = data["data"]["simRssi"] 144 | lastHeartbeatTime = data["data"]["lastHeartbeatTime"] 145 | lastReportTimeNew = data["data"]["lastReportTimeNew"] 146 | lost = data["data"]["lost"] 147 | smallBatteryIslose = data["data"]["smallBatteryIslose"] 148 | supportBleProtocol = data["data"]["supportBleProtocol"] 149 | mainBatteryEletricWitchDecimal = data["data"]["mainBatteryEletricWitchDecimal"] 150 | smartCharge = data["data"]["smartCharge"] 151 | mileage = data["data"]["mileage"] 152 | headLampState = data["data"]["headLampState"] 153 | lastGpsLocTime = data["data"]["lastGpsLocTime"] 154 | smallBatteryResidueDays = data["data"]["smallBatteryResidueDays"] 155 | referPosition = data["data"]["referPosition"] 156 | batteryPercentTimeStamp = data["data"]["batteryPercentTimeStamp"] 157 | mainBatLossPercent = data["data"]["mainBatLossPercent"] 158 | electricityLevel = data["data"]["electricityLevel"] 159 | batteryPercent = data["data"]["batteryPercent"] 160 | position = data["data"]["position"] 161 | lastReportTime = data["data"]["lastReportTime"] 162 | mainBatChargeLeftTime = data["data"]["mainBatChargeLeftTime"] 163 | positionTimeStamp = data["data"]["positionTimeStamp"] 164 | smallEletric = data["data"]["smallEletric"] 165 | lockStatus = data["data"]["lockStatus"] 166 | lockLocalTime = data["data"]["lockLocalTime"] 167 | lockStatusTimeStamp = data["data"]["lockStatusTimeStamp"] 168 | address = data["data"]["address"] 169 | batteryVoltage = int(data["data"]["batteryVoltage"])/1000 170 | smallBatteryPercent = data["data"]["smallBatteryPercent"] 171 | requestTime = data["data"]["requestTime"] 172 | 173 | querytime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 174 | lastreporttime = datetime.datetime.fromtimestamp(int(lastReportTime)/1000).strftime("%Y-%m-%d %H:%M:%S") 175 | lastreporttimenew = datetime.datetime.fromtimestamp(int(lastReportTimeNew)/1000).strftime("%Y-%m-%d %H:%M:%S") 176 | requesttime = datetime.datetime.fromtimestamp(int(requestTime)/1000).strftime("%Y-%m-%d %H:%M:%S") 177 | positiontime = datetime.datetime.fromtimestamp(int(positionTimeStamp)/1000).strftime("%Y-%m-%d %H:%M:%S") 178 | lockstatustime = datetime.datetime.fromtimestamp(int(lockStatusTimeStamp)/1000).strftime("%Y-%m-%d %H:%M:%S") 179 | speed = 0 180 | course = 0 181 | battery = batteryPercent 182 | 183 | if lockStatus == 0: 184 | acc = "已锁车" 185 | parkingtime = self.time_diff(int(time.mktime(time.strptime(lastreporttime, "%Y-%m-%d %H:%M:%S")))) 186 | elif lockStatus == 1: 187 | acc = "已启动" 188 | parkingtime = "" 189 | else: 190 | acc = "未知" 191 | 192 | if defenceStatus == 1: 193 | status = "已设防" 194 | elif defenceStatus == 0: 195 | status = "未设防" 196 | else: 197 | status = "未知" 198 | 199 | onlinestatus = "在线" if lost == 0 else "离线" 200 | _LOGGER.debug("position: %s", position) 201 | positions = list(map(float, position.split(","))) 202 | thislat = float(positions[1]) 203 | thislon = float(positions[0]) 204 | laststoptime = lastreporttime 205 | updatetime = positiontime 206 | if speed == 0: 207 | runorstop = "静止" 208 | else: 209 | runorstop = "运动" 210 | 211 | 212 | attrs = { 213 | "speed":speed, 214 | "course":course, 215 | "querytime":querytime, 216 | "laststoptime":laststoptime, 217 | "last_update":updatetime, 218 | "runorstop":runorstop, 219 | "parkingtime":parkingtime, 220 | "address":address, 221 | "onlinestatus":onlinestatus, 222 | "mileage":mileage, 223 | "defence":status, 224 | "acc":acc, 225 | "lockstatustime":lockstatustime, 226 | "battery":battery, 227 | "powbatteryvoltage":mainBatteryEletricWitchDecimal, 228 | "batteryvoltage":batteryVoltage, 229 | "smallBatteryPercent":smallBatteryPercent, 230 | "requesttime":requesttime, 231 | "lastreporttimenew":lastreporttimenew, 232 | "smartCharge":smartCharge 233 | } 234 | 235 | self.trackerdata[imei] = {"location_key":self.location_key+str(imei),"deviceinfo":self.deviceinfo[imei],"thislat":thislat,"thislon":thislon,"status":status,"attrs":attrs} 236 | 237 | return self.trackerdata 238 | 239 | 240 | 241 | class GetDataError(Exception): 242 | """request error or response data is unexpected""" 243 | 244 | 245 | class DataButton: 246 | 247 | def __init__(self, hass, username, password, device_imei): 248 | self.hass = hass 249 | self._username = username 250 | self._password = password 251 | self.device_imei = device_imei 252 | self.session_hellobike = requests.session() 253 | self.cloudpgs_token = None 254 | 255 | headers = { 256 | 'content-type': 'application/json; charset=utf-8', 257 | 'User-Agent': HELLOBIKE_USER_AGENT 258 | } 259 | self.session_hellobike.headers.update(headers) 260 | 261 | def _post_data(self, url, p_data): 262 | resp = self.session_hellobike.post(url, data=json.dumps(p_data)).json() 263 | return resp 264 | 265 | async def _action(self, action): 266 | json_body = { 267 | "bikeNo" : str(self.device_imei), 268 | "token" : self._password, 269 | "action" : action, 270 | "apiVersion": "2.23.0" 271 | } 272 | url = HELLOBIKE_API_URL + "?" + action 273 | 274 | try: 275 | async with timeout(10): 276 | resdata = await self.hass.async_add_executor_job(self._post_data, url, json_body) 277 | except ( 278 | ClientConnectorError 279 | ) as error: 280 | raise UpdateFailed(error) 281 | _LOGGER.debug("Requests remaining: %s", url) 282 | _LOGGER.debug(resdata) 283 | state = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 284 | _LOGGER.info("操作cloudgps: %s ", json_body) 285 | return state 286 | 287 | 288 | 289 | class DataSwitch: 290 | 291 | def __init__(self, hass, username, password, device_imei): 292 | self.hass = hass 293 | self._username = username 294 | self._password = password 295 | self.device_imei = device_imei 296 | self.session_hellobike = requests.session() 297 | self.cloudpgs_token = None 298 | 299 | headers = { 300 | 'content-type': 'application/json; charset=utf-8', 301 | 'User-Agent': HELLOBIKE_USER_AGENT 302 | } 303 | self.session_hellobike.headers.update(headers) 304 | 305 | def _post_data(self, url, p_data): 306 | resp = self.session_hellobike.post(url, data=json.dumps(p_data)).json() 307 | return resp 308 | 309 | async def _turn_on(self, action): 310 | if action == "defence": 311 | url = "https://a.hellobike.com/evehicle/api?rent.order.setUpDefence" 312 | json_body = { 313 | "action": "rent.order.setUpDefence", 314 | "maction": "SET_DEFENCE", 315 | "bikeNo": self.device_imei, 316 | "token": self._password, 317 | "apiVersion": "2.23.0" 318 | } 319 | await self.hass.async_add_executor_job(self._post_data, url, json_body) 320 | elif action == "open_lock": 321 | url = "https://a.hellobike.com/evehicle/api?rent.order.openLock" 322 | json_body = { 323 | "action": "rent.order.openLock", 324 | "bikeNo": self.device_imei, 325 | "token": self._password, 326 | "apiVersion": "2.23.0" 327 | } 328 | await self.hass.async_add_executor_job(self._post_data, url, json_body) 329 | 330 | async def _turn_off(self, action): 331 | if action == "defence": 332 | url = "https://a.hellobike.com/evehicle/api?rent.order.setUpDefence" 333 | json_body = { 334 | "action": "rent.order.setUpDefence", 335 | "maction": "WITHDRAW_DEFENCE", 336 | "bikeNo": self.device_imei, 337 | "token": self._password, 338 | "apiVersion": "2.23.0" 339 | } 340 | await self.hass.async_add_executor_job(self._post_data, url, json_body) 341 | elif action == "open_lock": 342 | url = "https://a.hellobike.com/evehicle/api?rent.order.openLock" 343 | json_body = { 344 | "action": "rent.order.closeLockCommand", 345 | "bikeNo": self.device_imei, 346 | "token": self._password, 347 | "apiVersion": "2.23.0" 348 | } 349 | await self.hass.async_add_executor_job(self._post_data, url, json_body) -------------------------------------------------------------------------------- /custom_components/cloud_gps/helper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # form https://github.com/wandergis/coordTransform_py/blob/master/coordTransform_utils.py 3 | 4 | 5 | """Mars coordinates transform""" 6 | import math 7 | 8 | pi = 3.1415926535897932384626 # π 9 | a = 6378245.0 # 长半轴 10 | ee = 0.00669342162296594323 # 扁率 11 | 12 | def wgs84togcj02(lng, lat): 13 | """ 14 | WGS84转GCJ02(火星坐标系) 15 | :param lng:WGS84坐标系的经度 16 | :param lat:WGS84坐标系的纬度 17 | :return: 18 | """ 19 | if out_of_china(lng, lat): # 判断是否在国内 20 | return lng, lat 21 | dlat = transformlat(lng - 105.0, lat - 35.0) 22 | dlng = transformlng(lng - 105.0, lat - 35.0) 23 | radlat = lat / 180.0 * pi 24 | magic = math.sin(radlat) 25 | magic = 1 - ee * magic * magic 26 | sqrtmagic = math.sqrt(magic) 27 | dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * pi) 28 | dlng = (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * pi) 29 | mglat = lat + dlat 30 | mglng = lng + dlng 31 | return [mglng, mglat] 32 | 33 | 34 | def gcj02towgs84(lng, lat): 35 | """ 36 | GCJ02(火星坐标系)转GPS84 37 | :param lng:火星坐标系的经度 38 | :param lat:火星坐标系纬度 39 | :return: 40 | """ 41 | if out_of_china(lng, lat): 42 | return [lng, lat] 43 | dlat = transformlat(lng - 105.0, lat - 35.0) 44 | dlng = transformlng(lng - 105.0, lat - 35.0) 45 | radlat = lat / 180.0 * pi 46 | magic = math.sin(radlat) 47 | magic = 1 - ee * magic * magic 48 | sqrtmagic = math.sqrt(magic) 49 | dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * pi) 50 | dlng = (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * pi) 51 | mglat = lat + dlat 52 | mglng = lng + dlng 53 | return [lng * 2 - mglng, lat * 2 - mglat] 54 | 55 | def gcj02_to_bd09(lng, lat): 56 | """ 57 | 火星坐标系(GCJ-02)转百度坐标系(BD-09) 58 | 谷歌、高德——>百度 59 | :param lng:火星坐标经度 60 | :param lat:火星坐标纬度 61 | :return: 62 | """ 63 | z = math.sqrt(lng * lng + lat * lat) + 0.00002 * math.sin(lat * pi) 64 | theta = math.atan2(lat, lng) + 0.000003 * math.cos(lng * pi) 65 | bd_lng = z * math.cos(theta) + 0.0065 66 | bd_lat = z * math.sin(theta) + 0.006 67 | return [bd_lng, bd_lat] 68 | 69 | 70 | def bd09_to_gcj02(bd_lon, bd_lat): 71 | """ 72 | 百度坐标系(BD-09)转火星坐标系(GCJ-02) 73 | 百度——>谷歌、高德 74 | :param bd_lat:百度坐标纬度 75 | :param bd_lon:百度坐标经度 76 | :return:转换后的坐标列表形式 77 | """ 78 | x = bd_lon - 0.0065 79 | y = bd_lat - 0.006 80 | z = math.sqrt(x * x + y * y) - 0.00002 * math.sin(y * pi) 81 | theta = math.atan2(y, x) - 0.000003 * math.cos(x * pi) 82 | gg_lng = z * math.cos(theta) 83 | gg_lat = z * math.sin(theta) 84 | return [gg_lng, gg_lat] 85 | 86 | def bd09_to_wgs84(bd_lon, bd_lat): 87 | lon, lat = bd09_to_gcj02(bd_lon, bd_lat) 88 | return gcj02towgs84(lon, lat) 89 | 90 | 91 | def wgs84_to_bd09(lon, lat): 92 | lon, lat = wgs84togcj02(lon, lat) 93 | return gcj02_to_bd09(lon, lat) 94 | 95 | 96 | def transformlat(lng, lat): 97 | ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * math.sqrt(math.fabs(lng)) 98 | ret += (20.0 * math.sin(6.0 * lng * pi) + 20.0 * 99 | math.sin(2.0 * lng * pi)) * 2.0 / 3.0 100 | ret += (20.0 * math.sin(lat * pi) + 40.0 * 101 | math.sin(lat / 3.0 * pi)) * 2.0 / 3.0 102 | ret += (160.0 * math.sin(lat / 12.0 * pi) + 320 * 103 | math.sin(lat * pi / 30.0)) * 2.0 / 3.0 104 | return ret 105 | 106 | 107 | def transformlng(lng, lat): 108 | ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * math.sqrt(math.fabs(lng)) 109 | ret += (20.0 * math.sin(6.0 * lng * pi) + 20.0 * 110 | math.sin(2.0 * lng * pi)) * 2.0 / 3.0 111 | ret += (20.0 * math.sin(lng * pi) + 40.0 * 112 | math.sin(lng / 3.0 * pi)) * 2.0 / 3.0 113 | ret += (150.0 * math.sin(lng / 12.0 * pi) + 300.0 * 114 | math.sin(lng / 30.0 * pi)) * 2.0 / 3.0 115 | return ret 116 | 117 | 118 | def out_of_china(lng, lat): 119 | """ 120 | 判断是否在国内,不在国内不做偏移 121 | :param lng: 122 | :param lat: 123 | :return: 124 | """ 125 | if lng < 72.004 or lng > 137.8347: 126 | return True 127 | if lat < 0.8293 or lat > 55.8271: 128 | return True 129 | return False 130 | 131 | if __name__ == '__main__': 132 | lng = 121.532 133 | lat = 31.256 134 | result1 = wgs84togcj02(lng, lat) 135 | result2 = gcj02towgs84(result1[0], result1[1]) 136 | print(result1, result2) -------------------------------------------------------------------------------- /custom_components/cloud_gps/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "cloud_gps", 3 | "name": "云平台GPS", 4 | "codeowners": ["@dscao"], 5 | "config_flow": true, 6 | "dependencies": [], 7 | "documentation": "https://github.com/dscao/cloud_gps", 8 | "iot_class": "cloud_polling", 9 | "issue_tracker": "https://github.com/dscao/cloud_gps/issues", 10 | "requirements": [], 11 | "version": "2025.5.7" 12 | } -------------------------------------------------------------------------------- /custom_components/cloud_gps/niu_data_fetcher.py: -------------------------------------------------------------------------------- 1 | """ 2 | get info 3 | 请求数据的核心代码来源: https://github.com/goxofy/home-assistant-niu-component/blob/master/custom_components/niu/sensor.py 4 | """ 5 | 6 | import logging 7 | import requests 8 | import re 9 | import asyncio 10 | import json 11 | import time 12 | import datetime 13 | import hashlib 14 | from time import gmtime, strftime 15 | from async_timeout import timeout 16 | from aiohttp.client_exceptions import ClientConnectorError 17 | from homeassistant.helpers.aiohttp_client import async_create_clientsession 18 | from homeassistant.helpers.update_coordinator import UpdateFailed 19 | from urllib3.util.retry import Retry 20 | from requests.adapters import HTTPAdapter 21 | from homeassistant.const import ( 22 | CONF_USERNAME, 23 | CONF_PASSWORD, 24 | CONF_CLIENT_ID, 25 | ) 26 | 27 | from .const import ( 28 | COORDINATOR, 29 | DOMAIN, 30 | CONF_WEB_HOST, 31 | CONF_DEVICE_IMEI, 32 | UNDO_UPDATE_LISTENER, 33 | CONF_ATTR_SHOW, 34 | CONF_UPDATE_INTERVAL, 35 | ) 36 | 37 | _LOGGER = logging.getLogger(__name__) 38 | 39 | NIU_USER_AGENT = 'manager/4.6.48 (android; IN2020 11);lang=zh-CN;clientIdentifier=Domestic;timezone=Asia/Shanghai;model=IN2020;deviceName=IN2020;ostype=android' 40 | NIU_API_HOST_TOKEN = "https://account.niu.com" 41 | NIU_API_HOST_TRACKER = "https://app-api.niu.com" 42 | NIU_LOGIN_URI = "/v3/api/oauth2/token" 43 | NIU_MOTOR_BATTERY_API_URI = "/v3/motor_data/battery_info" 44 | NIU_MOTOR_INDEX_API_URI = "/v5/scooter/motor_data/index_info" 45 | NIU_MOTOINFO_LIST_API_URI = "/v5/scooter/list" 46 | NIU_MOTOINFO_ALL_API_URI = "/motoinfo/overallTally" 47 | NIU_TRACK_LIST_API_URI = "/v5/track/list/v2" 48 | 49 | class DataFetcher: 50 | """fetch the cloud gps data""" 51 | 52 | def __init__(self, hass, username, password, device_imei, location_key): 53 | self.hass = hass 54 | self.location_key = location_key 55 | self.username = username 56 | self.password = password 57 | self.device_imei = device_imei 58 | #self.session_niu = requests.session() 59 | self.cloudpgs_token = None 60 | self._lat_old = 0 61 | self._lon_old = 0 62 | self.deviceinfo = {} 63 | self.trackerdata = {} 64 | self.address = {} 65 | self.totalkm = {} 66 | 67 | headers = { 68 | 'User-Agent': NIU_USER_AGENT 69 | } 70 | #self.session_niu.headers.update(headers) 71 | 72 | def _get_niu_token(self, username, password): 73 | url = NIU_API_HOST_TOKEN + '/v3/api/oauth2/token' 74 | md5 = hashlib.md5(password.encode("utf-8")).hexdigest() 75 | data = { 76 | "account": username, 77 | "password": md5, 78 | "grant_type": "password", 79 | "scope": "base", 80 | "app_id": "niu_ktdrr960", 81 | } 82 | try: 83 | r = requests.post(url, data=data) 84 | except BaseException as e: 85 | print(e) 86 | return False 87 | data = json.loads(r.content.decode()) 88 | return data["data"]["token"]["access_token"] 89 | 90 | 91 | def _get_niu_vehicles_info(self, token): 92 | 93 | url = NIU_API_HOST_TRACKER + '/v5/scooter/list' 94 | headers = {"token": token} 95 | try: 96 | r = requests.get(url, headers=headers, data=[]) 97 | except ConnectionError: 98 | return False 99 | if r.status_code != 200: 100 | return False 101 | data = json.loads(r.content.decode()) 102 | return data 103 | 104 | 105 | def _get_niu_info(self, path, sn, token): 106 | url = NIU_API_HOST_TRACKER + path 107 | 108 | params = {"sn": sn} 109 | headers = { 110 | "token": token, 111 | "Accept-Language": "en-US", 112 | "user-agent": NIU_USER_AGENT 113 | } 114 | try: 115 | 116 | r = requests.get(url, headers=headers, params=params) 117 | 118 | except ConnectionError: 119 | return False 120 | if r.status_code != 200: 121 | return False 122 | data = json.loads(r.content.decode()) 123 | if data["status"] != 0: 124 | return False 125 | return data 126 | 127 | 128 | def _post_niu_info(self, path, sn, token): 129 | url = NIU_API_HOST_TRACKER + path 130 | params = {} 131 | headers = { 132 | "token": token, 133 | "Accept-Language": "en-US", 134 | "User-Agent": NIU_USER_AGENT 135 | } 136 | try: 137 | r = requests.post(url, headers=headers, params=params, data={"sn": sn}) 138 | except ConnectionError: 139 | return False 140 | if r.status_code != 200: 141 | return False 142 | data = json.loads(r.content.decode()) 143 | if data["status"] != 0: 144 | return False 145 | return data 146 | 147 | 148 | def _post_niu_info_track(self, path, sn, token): 149 | url = NIU_API_HOST_TRACKER + path 150 | params = {} 151 | headers = { 152 | "token": token, 153 | "Accept-Language": "en-US", 154 | "User-Agent": NIU_USER_AGENT 155 | } 156 | try: 157 | r = requests.post( 158 | url, 159 | headers=headers, 160 | params=params, 161 | json={"index": "0", "pagesize": 10, "sn": sn}, 162 | ) 163 | except ConnectionError: 164 | return False 165 | if r.status_code != 200: 166 | return False 167 | data = json.loads(r.content.decode()) 168 | if data["status"] != 0: 169 | return False 170 | return data 171 | 172 | def time_diff(self, timestamp): 173 | result = datetime.datetime.now() - datetime.datetime.fromtimestamp(timestamp) 174 | hours = int(result.seconds / 3600) 175 | minutes = int(result.seconds % 3600 / 60) 176 | seconds = result.seconds%3600%60 177 | if result.days > 0: 178 | return("{0}天{1}小时{2}分钟".format(result.days,hours,minutes)) 179 | elif hours > 0: 180 | return("{0}小时{1}分钟".format(hours,minutes)) 181 | elif minutes > 0: 182 | return("{0}分钟{1}秒".format(minutes,seconds)) 183 | else: 184 | return("{0}秒".format(seconds)) 185 | 186 | async def get_data(self): 187 | 188 | if self.cloudpgs_token is None: 189 | self.cloudpgs_token = await self.hass.async_add_executor_job(self._get_niu_token, self.username, self.password) 190 | _LOGGER.debug("get niu token: %s", self.cloudpgs_token) 191 | if self.cloudpgs_token: 192 | deviceslistinfo = await self.hass.async_add_executor_job(self._get_niu_vehicles_info, self.cloudpgs_token) 193 | for deviceinfo in deviceslistinfo["data"]["items"]: 194 | self.deviceinfo[str(deviceinfo["sn_id"])] = {} 195 | for deviceinfo in deviceslistinfo["data"]["items"]: 196 | self.deviceinfo[str(deviceinfo["sn_id"])]["device_model"] = "小牛电动车" 197 | self.deviceinfo[str(deviceinfo["sn_id"])]["sw_version"] = "未知" 198 | self.deviceinfo[str(deviceinfo["sn_id"])]["expiration"] = "永久" 199 | 200 | for imei in self.device_imei: 201 | _LOGGER.debug("Requests imei: %s", imei) 202 | 203 | if not self.deviceinfo.get(imei): 204 | self.deviceinfo[imei] = {} 205 | try: 206 | async with timeout(10): 207 | infodata = await self.hass.async_add_executor_job(self.post_niu_info, imei) 208 | except ClientConnectorError as error: 209 | _LOGGER.error("连接错误: %s", error) 210 | except asyncio.TimeoutError: 211 | _LOGGER.error("获取数据超时 (10秒)") 212 | except Exception as e: 213 | _LOGGER.error("未知错误: %s", repr(e)) 214 | finally: 215 | _LOGGER.debug("最终数据结果: %s", infodata) 216 | 217 | if infodata: 218 | self.deviceinfo[imei] =infodata 219 | self.deviceinfo[imei]["device_model"] = "小牛电动车" 220 | self.deviceinfo[imei]["sw_version"] = "未知" 221 | self.deviceinfo[imei]["expiration"] = "永久" 222 | 223 | 224 | self.batterydata[imei] = {} 225 | try: 226 | async with timeout(10): 227 | batterydata = await self.hass.async_add_executor_job(self._get_niu_info, "/v3/motor_data/battery_info", imei, self.cloudpgs_token) 228 | except Exception as error: 229 | raise 230 | _LOGGER.debug("result battery data: %s", batterydata) 231 | if batterydata: 232 | self.batterydata[imei] = { 233 | "BatteryCharge": batterydata["data"]["batteries"]["compartmentA"]["batteryCharging"], 234 | "BatteryIsconnected": batterydata["data"]["batteries"]["compartmentA"]["isConnected"], 235 | "BatteryTimesCharged": batterydata["data"]["batteries"]["compartmentA"]["chargedTimes"], 236 | "BatterytemperatureDesc": batterydata["data"]["batteries"]["compartmentA"]["temperatureDesc"], 237 | "BatteryTemperature": batterydata["data"]["batteries"]["compartmentA"]["temperature"], 238 | "BatteryGrade": batterydata["data"]["batteries"]["compartmentA"]["gradeBattery"] 239 | } 240 | 241 | 242 | self.motoinfodata[imei] = {} 243 | try: 244 | async with timeout(10): 245 | motoinfodata = await self.hass.async_add_executor_job(self._post_niu_info, "/motoinfo/overallTally", imei, self.cloudpgs_token) 246 | except Exception as error: 247 | raise 248 | _LOGGER.debug("result motoinfo data: %s", motoinfodata) 249 | if motoinfodata: 250 | self.motoinfodata[imei] = { 251 | "totalMileage": motoinfodata["data"]["totalMileage"], 252 | "DaysInUse": motoinfodata["data"]["bindDaysCount"] 253 | } 254 | 255 | 256 | self.infotrackdata[imei] = {} 257 | try: 258 | async with timeout(10): 259 | infotrackdata = await self.hass.async_add_executor_job(self._post_niu_info_track, "/v5/track/list/v2", imei, self.cloudpgs_token) 260 | except Exception as error: 261 | raise 262 | _LOGGER.debug("result infotrack data: %s", infotrackdata) 263 | if infotrackdata: 264 | self.infotrackdata[imei] = { 265 | "LastTrackStartTime": datetime.fromtimestamp((infotrackdata["data"][0]["startTime"]) / 1000 ).strftime("%Y-%m-%d %H:%M:%S"), 266 | "LastTrackEndTime": datetime.fromtimestamp((infotrackdata["data"][0]["endTime"]) / 1000 ).strftime("%Y-%m-%d %H:%M:%S"), 267 | "LastTrackDistance": infotrackdata["data"][0]["distance"], 268 | "LastTrackAverageSpeed":infotrackdata["data"][0]["avespeed"], 269 | "LastTrackRidingtime": strftime("%H:%M:%S", gmtime(infotrackdata["data"][0]["ridingtime"])) 270 | "LastTrackThumb": infotrackdata["data"][0]["track_thumb"].replace("app-api.niucache.com", "app-api.niu.com"} 271 | 272 | 273 | 274 | self.motodata[imei] = {} 275 | try: 276 | async with timeout(10): 277 | motodata = await self.hass.async_add_executor_job(self._get_niu_info, "/v5/scooter/motor_data/index_info", imei, self.cloudpgs_token) 278 | except Exception as error: 279 | raise 280 | _LOGGER.debug("result moto data: %s", motodata) 281 | if motodata: 282 | self.motodata[imei] = { 283 | "CurrentSpeed": motodata["data"]["nowSpeed"], 284 | "ScooterConnected": motodata["data"]["isConnected"], 285 | "IsCharging": motodata["data"]["isCharging"], 286 | "IsLocked": motodata["data"]["lockStatus"], 287 | "TimeLeft": motodata["data"]["leftTime"], 288 | "EstimatedMileage": motodata["data"]["estimatedMileage"], 289 | "centreCtrlBatt": motodata["data"]["centreCtrlBattery"], 290 | "HDOP": motodata["data"]["hdop"], 291 | "Distance": motodata["data"]["lastTrack"]["distance"], 292 | "RidingTime": motodata["data"]["lastTrack"]["ridingTime"], 293 | "Longitude": motodata["data"]["postion"]["lng"], 294 | "Latitude": motodata["data"]["postion"]["lat"], 295 | } 296 | 297 | self.trackerdata[imei] = {} 298 | 299 | if self.motodata[imei]: 300 | querytime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 301 | updatetime = "" 302 | course = "" 303 | speed = float(self.motodata[imei]["CurrentSpeed"]) 304 | _LOGGER.debug("speed: %s", speed) 305 | 306 | status = "停车" 307 | 308 | if self.motodata[imei]["IsLocked"] == 1: 309 | acc = "已锁车" 310 | elif data["HD_STATE"] == 0: 311 | acc = "已开锁" 312 | status = "钥匙开启" 313 | else: 314 | acc = "未知" 315 | 316 | thislat = float(self.motodata[imei]["Latitude"]) 317 | thislon = float(self.motodata[imei]["Longitude"]) 318 | laststoptime = self.motodata[imei]["TimeLeft"] 319 | parkingtime = self.time_diff(int(time.mktime(time.strptime(laststoptime, "%Y-%m-%d %H:%M:%S")))) 320 | positionType = "GPS" 321 | if speed == 0: 322 | runorstop = "静止" 323 | else: 324 | runorstop = "运动" 325 | status = "行驶" 326 | 327 | if self.motodata[imei]["ScooterConnected"] == 1: 328 | onlinestatus = "在线" 329 | elif data["HD_STATE"] == 0: 330 | onlinestatus = "离线" 331 | status = "离线" 332 | else: 333 | onlinestatusstatus = "未知" 334 | 335 | attrs = { 336 | "speed":speed, 337 | "course":course, 338 | "querytime":querytime, 339 | "laststoptime":laststoptime, 340 | "last_update":updatetime, 341 | "acc":acc, 342 | "runorstop":runorstop, 343 | "onlinestatus", onlinestatus, 344 | "parkingtime":parkingtime 345 | } 346 | 347 | attrs.update(self.infotrackdata[imei]) 348 | attrs.update(self.batterydata[imei]) 349 | 350 | self.trackerdata[imei] = {"location_key":self.location_key+str(imei),"deviceinfo":self.deviceinfo[imei],"thislat":thislat,"thislon":thislon,"status":status,"attrs": attrs} 351 | 352 | return self.trackerdata 353 | 354 | 355 | class GetDataError(Exception): 356 | """request error or response data is unexpected""" 357 | -------------------------------------------------------------------------------- /custom_components/cloud_gps/sensor.py: -------------------------------------------------------------------------------- 1 | """sensor Entities.""" 2 | import logging 3 | import time, datetime 4 | from homeassistant.helpers.update_coordinator import CoordinatorEntity 5 | from homeassistant.helpers.device_registry import DeviceEntryType 6 | from homeassistant.components.sensor import ( 7 | SensorDeviceClass, 8 | SensorEntity, 9 | SensorEntityDescription, 10 | SensorStateClass, 11 | ) 12 | from homeassistant.config_entries import ConfigEntry 13 | from homeassistant.core import HomeAssistant, callback 14 | from homeassistant.helpers.dispatcher import async_dispatcher_connect 15 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 16 | 17 | from .const import ( 18 | COORDINATOR, 19 | DOMAIN, 20 | CONF_WEB_HOST, 21 | CONF_SENSORS, 22 | KEY_ADDRESS, 23 | KEY_LASTSTOPTIME, 24 | KEY_PARKING_TIME, 25 | KEY_SPEED, 26 | KEY_TOTALKM, 27 | KEY_STATUS, 28 | KEY_ACC, 29 | KEY_BATTERY, 30 | ) 31 | 32 | _LOGGER = logging.getLogger(__name__) 33 | 34 | SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( 35 | SensorEntityDescription( 36 | key=KEY_ADDRESS, 37 | name="address", 38 | icon="mdi:map" 39 | ), 40 | SensorEntityDescription( 41 | key=KEY_PARKING_TIME, 42 | name="parkingtime", 43 | icon="mdi:parking" 44 | ), 45 | SensorEntityDescription( 46 | key=KEY_LASTSTOPTIME, 47 | name="laststoptime", 48 | icon="mdi:timer-stop" 49 | ), 50 | SensorEntityDescription( 51 | key=KEY_SPEED, 52 | name="speed", 53 | unit_of_measurement = "km/h", 54 | device_class = "speed" 55 | ), 56 | SensorEntityDescription( 57 | key=KEY_TOTALKM, 58 | name="totalkm", 59 | unit_of_measurement = "km", 60 | device_class = "distance" 61 | ), 62 | SensorEntityDescription( 63 | key=KEY_STATUS, 64 | name="status", 65 | icon="mdi:car-brake-alert" 66 | ), 67 | SensorEntityDescription( 68 | key=KEY_ACC, 69 | name="acc", 70 | icon="mdi:engine" 71 | ), 72 | SensorEntityDescription( 73 | key=KEY_BATTERY, 74 | name="powbattery", 75 | unit_of_measurement = "V", 76 | icon="mdi:car-battery" 77 | ) 78 | ) 79 | 80 | SENSOR_TYPES_MAP = { description.key: description for description in SENSOR_TYPES } 81 | #_LOGGER.debug("SENSOR_TYPES_MAP: %s" ,SENSOR_TYPES_MAP) 82 | 83 | SENSOR_TYPES_KEYS = { description.key for description in SENSOR_TYPES } 84 | #_LOGGER.debug("SENSOR_TYPES_KEYS: %s" ,SENSOR_TYPES_KEYS) 85 | 86 | async def async_setup_entry(hass, config_entry, async_add_entities): 87 | """Add tuqiang entities from a config_entry.""" 88 | coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] 89 | webhost = config_entry.data[CONF_WEB_HOST] 90 | enabled_sensors = [s for s in config_entry.options.get(CONF_SENSORS, []) if s in SENSOR_TYPES_KEYS] 91 | 92 | _LOGGER.debug("coordinator sensors: %s", coordinator.data) 93 | _LOGGER.debug("enabled_sensors: %s" ,enabled_sensors) 94 | 95 | for coordinatordata in coordinator.data: 96 | _LOGGER.debug("coordinatordata") 97 | _LOGGER.debug(coordinatordata) 98 | 99 | sensors = [] 100 | for sensor_type in enabled_sensors: 101 | _LOGGER.debug("sensor_type: %s" ,sensor_type) 102 | sensors.append(CloudGPSSensorEntity(webhost, coordinatordata, SENSOR_TYPES_MAP[sensor_type], coordinator)) 103 | 104 | async_add_entities(sensors, False) 105 | 106 | class CloudGPSSensorEntity(CoordinatorEntity): 107 | """Define an sensor entity.""" 108 | 109 | _attr_has_entity_name = True 110 | 111 | def __init__(self, webhost, imei, description, coordinator): 112 | """Initialize.""" 113 | super().__init__(coordinator) 114 | self.entity_description = description 115 | self._webhost = webhost 116 | self._imei = imei 117 | self.coordinator = coordinator 118 | _LOGGER.debug("SensorEntity coordinator: %s", coordinator.data) 119 | self._unique_id = f"{self.coordinator.data[self._imei]['location_key']}-{description.key}" 120 | 121 | self._attr_translation_key = f"{self.entity_description.name}" 122 | if self.entity_description.key == "parkingtime": 123 | self._state = self.coordinator.data[self._imei]["attrs"].get("parkingtime") 124 | self._attrs = {"querytime": self.coordinator.data[self._imei]["attrs"].get("querytime")} 125 | elif self.entity_description.key == "laststoptime": 126 | self._state = self.coordinator.data[self._imei]["attrs"].get("laststoptime") 127 | self._attrs = {"querytime": self.coordinator.data[self._imei]["attrs"].get("querytime")} 128 | elif self.entity_description.key == "address": 129 | self._state = self.coordinator.data[self._imei]["attrs"].get("address") 130 | self._attrs = {"querytime": self.coordinator.data[self._imei]["attrs"].get("querytime")} 131 | elif self.entity_description.key == "speed": 132 | self._state = float(self.coordinator.data[self._imei]["attrs"].get("speed", 0)) 133 | self._attrs = {"querytime": self.coordinator.data[self._imei]["attrs"].get("querytime")} 134 | elif self.entity_description.key == "totalkm": 135 | self._state = float(self.coordinator.data[self._imei]["attrs"].get("totalKm", 0)) 136 | self._attrs = {"querytime": self.coordinator.data[self._imei]["attrs"].get("querytime")} 137 | elif self.entity_description.key == "acc": 138 | self._state = self.coordinator.data[self._imei]["attrs"].get("acc") 139 | self._attrs = {"querytime": self.coordinator.data[self._imei]["attrs"].get("querytime")} 140 | elif self.entity_description.key == "powbattery": 141 | self._state = float(self.coordinator.data[self._imei]["attrs"].get("powbatteryvoltage", 0)) 142 | self._attrs = {"querytime": self.coordinator.data[self._imei]["attrs"].get("querytime")} 143 | elif self.entity_description.key == "status": 144 | self._state = self.coordinator.data[self._imei].get("status") 145 | self._attrs = {"querytime": self.coordinator.data[self._imei]["attrs"].get("querytime")} 146 | 147 | _LOGGER.debug(self._state) 148 | 149 | @property 150 | def unique_id(self): 151 | return self._unique_id 152 | 153 | @property 154 | def device_info(self): 155 | """Return the device info.""" 156 | return { 157 | "identifiers": {(DOMAIN, self.coordinator.data[self._imei]["location_key"])}, 158 | "name": self._imei, 159 | "manufacturer": self._webhost, 160 | "entry_type": DeviceEntryType.SERVICE, 161 | "model": self.coordinator.data[self._imei]["deviceinfo"]["device_model"], 162 | "sw_version": self.coordinator.data[self._imei]["deviceinfo"]["sw_version"], 163 | } 164 | 165 | @property 166 | def should_poll(self): 167 | """Return the polling requirement of the entity.""" 168 | return True 169 | 170 | @property 171 | def native_value(self): 172 | """Return battery value of the device.""" 173 | return self._state 174 | 175 | @property 176 | def state(self): 177 | """Return the state.""" 178 | return self._state 179 | 180 | @property 181 | def unit_of_measurement(self): 182 | """Return the unit_of_measurement.""" 183 | if self.entity_description.unit_of_measurement: 184 | return self.entity_description.unit_of_measurement 185 | 186 | @property 187 | def device_class(self): 188 | """Return the unit_of_measurement.""" 189 | if self.entity_description.device_class: 190 | return self.entity_description.device_class 191 | 192 | @property 193 | def state_attributes(self): 194 | attrs = {} 195 | if self.coordinator.data.get(self._imei): 196 | attrs["querytime"] = self.coordinator.data[self._imei]["attrs"]["querytime"] 197 | return attrs 198 | 199 | async def async_added_to_hass(self): 200 | """Connect to dispatcher listening for entity data notifications.""" 201 | self.async_on_remove( 202 | self.coordinator.async_add_listener(self.async_write_ha_state) 203 | ) 204 | 205 | async def async_update(self): 206 | """Update tuqiang entity.""" 207 | _LOGGER.debug("刷新sensor数据") 208 | #await self.coordinator.async_request_refresh() 209 | if self.coordinator.data.get(self._imei): 210 | if self.entity_description.key == "parkingtime": 211 | self._state = self.coordinator.data[self._imei]["attrs"].get("parkingtime") 212 | self._attrs = {"querytime": self.coordinator.data[self._imei]["attrs"].get("querytime")} 213 | elif self.entity_description.key == "laststoptime": 214 | self._state = self.coordinator.data[self._imei]["attrs"].get("laststoptime") 215 | self._attrs = {"querytime": self.coordinator.data[self._imei]["attrs"].get("querytime")} 216 | elif self.entity_description.key == "address": 217 | self._state = self.coordinator.data[self._imei]["attrs"].get("address") 218 | self._attrs = {"querytime": self.coordinator.data[self._imei]["attrs"].get("querytime")} 219 | elif self.entity_description.key == "speed": 220 | self._state = float(self.coordinator.data[self._imei]["attrs"].get("speed", 0)) 221 | self._attrs = {"querytime": self.coordinator.data[self._imei]["attrs"].get("querytime")} 222 | elif self.entity_description.key == "totalkm": 223 | self._state = float(self.coordinator.data[self._imei]["attrs"].get("totalKm", 0)) 224 | self._attrs = {"querytime": self.coordinator.data[self._imei]["attrs"].get("querytime")} 225 | elif self.entity_description.key == "acc": 226 | self._state = self.coordinator.data[self._imei]["attrs"].get("acc") 227 | self._attrs = {"querytime": self.coordinator.data[self._imei]["attrs"].get("querytime")} 228 | elif self.entity_description.key == "powbattery": 229 | self._state = float(self.coordinator.data[self._imei]["attrs"].get("powbatteryvoltage", 0)) 230 | self._attrs = {"querytime": self.coordinator.data[self._imei]["attrs"].get("querytime")} 231 | elif self.entity_description.key == "status": 232 | self._state = self.coordinator.data[self._imei].get("status") 233 | self._attrs = {"querytime": self.coordinator.data[self._imei]["attrs"].get("querytime")} 234 | 235 | 236 | 237 | 238 | -------------------------------------------------------------------------------- /custom_components/cloud_gps/switch.py: -------------------------------------------------------------------------------- 1 | """switch Entities""" 2 | import logging 3 | import time 4 | import datetime 5 | import json 6 | import requests 7 | from async_timeout import timeout 8 | from aiohttp.client_exceptions import ClientConnectorError 9 | from homeassistant.helpers.update_coordinator import CoordinatorEntity 10 | from homeassistant.helpers.device_registry import DeviceEntryType 11 | from homeassistant.components.switch import ( 12 | SwitchEntity, 13 | SwitchEntityDescription 14 | ) 15 | 16 | from homeassistant.const import ( 17 | CONF_USERNAME, 18 | CONF_PASSWORD, 19 | ) 20 | 21 | from .const import ( 22 | COORDINATOR, 23 | DOMAIN, 24 | CONF_WEB_HOST, 25 | CONF_SWITCHS, 26 | ) 27 | 28 | 29 | _LOGGER = logging.getLogger(__name__) 30 | 31 | HELLOBIKE_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.43(0x18002b2d) NetType/4G Language/zh_CN' 32 | API_URL_HELLOBIKE = "https://a.hellobike.com/evehicle/api" 33 | 34 | SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( 35 | SwitchEntityDescription( 36 | key="defence", 37 | name="defence", 38 | icon="mdi:shield" 39 | ), 40 | SwitchEntityDescription( 41 | key="open_lock", 42 | name="open_lock", 43 | icon="mdi:lock-open" 44 | ), 45 | SwitchEntityDescription( 46 | key="defencemode", 47 | name="defencemode", 48 | icon="mdi:lock-open" 49 | ) 50 | ) 51 | 52 | SWITCH_TYPES_MAP = { description.key: description for description in SWITCH_TYPES } 53 | #_LOGGER.debug("SWITCH_TYPES_MAP: %s" ,SWITCH_TYPES_MAP) 54 | 55 | SWITCH_TYPES_KEYS = { description.key for description in SWITCH_TYPES } 56 | #_LOGGER.debug("SWITCH_TYPES_KEYS: %s" ,SWITCH_TYPES_KEYS) 57 | 58 | 59 | async def async_setup_entry(hass, config_entry, async_add_entities): 60 | """Add Switchentities from a config_entry.""" 61 | coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] 62 | webhost = config_entry.data[CONF_WEB_HOST] 63 | username = config_entry.data[CONF_USERNAME] 64 | password = config_entry.data[CONF_PASSWORD] 65 | enabled_switchs = [s for s in config_entry.options.get(CONF_SWITCHS, []) if s in SWITCH_TYPES_KEYS] 66 | 67 | _LOGGER.debug("coordinator switchs: %s", coordinator.data) 68 | _LOGGER.debug("enabled_switchs: %s" ,enabled_switchs) 69 | 70 | for coordinatordata in coordinator.data: 71 | _LOGGER.debug("coordinatordata") 72 | _LOGGER.debug(coordinatordata) 73 | 74 | switchs = [] 75 | for switch_type in enabled_switchs: 76 | _LOGGER.debug("switch_type: %s" ,switch_type) 77 | switchs.append(CloudGPSSwitchEntity(hass, webhost, username, password, coordinatordata, SWITCH_TYPES_MAP[switch_type], coordinator)) 78 | 79 | async_add_entities(switchs, False) 80 | 81 | 82 | class CloudGPSSwitchEntity(SwitchEntity): 83 | """Define an switch entity.""" 84 | _attr_has_entity_name = True 85 | 86 | def __init__(self, hass, webhost, username, password, imei, description, coordinator): 87 | """Initialize.""" 88 | super().__init__() 89 | self.entity_description = description 90 | self.session_hellobike = requests.session() 91 | self._hass = hass 92 | self._webhost = webhost 93 | self._username = username 94 | self._password = password 95 | self._imei = imei 96 | self.coordinator = coordinator 97 | _LOGGER.debug("SwitchEntity coordinator: %s", coordinator.data) 98 | self._unique_id = f"{self.coordinator.data[self._imei]['location_key']}-{description.key}" 99 | self._attr_translation_key = f"{self.entity_description.name}" 100 | 101 | self._is_on = None 102 | self._doing = False 103 | 104 | if webhost == "tuqiang123.com": 105 | from .tuqiang123_data_fetcher import DataSwitch 106 | elif webhost == "hellobike.com": 107 | from .hellobike_data_fetcher import DataSwitch 108 | else: 109 | _LOGGER.error("配置的实体平台不支持,请不要启用此按钮实体!") 110 | return 111 | 112 | self._switch = DataSwitch(hass, username, password, imei) 113 | 114 | 115 | 116 | @property 117 | def unique_id(self): 118 | return self._unique_id 119 | 120 | @property 121 | def device_info(self): 122 | """Return the device info.""" 123 | return { 124 | "identifiers": {(DOMAIN, self.coordinator.data[self._imei]["location_key"])}, 125 | "name": self._imei, 126 | "manufacturer": self._webhost, 127 | "entry_type": DeviceEntryType.SERVICE, 128 | "model": self.coordinator.data[self._imei]["deviceinfo"]["device_model"], 129 | "sw_version": self.coordinator.data[self._imei]["deviceinfo"]["sw_version"], 130 | } 131 | 132 | 133 | @property 134 | def should_poll(self): 135 | """Return the polling requirement of the entity.""" 136 | return True 137 | 138 | @property 139 | def is_on(self): 140 | """Check if switch is on.""" 141 | return self._is_on 142 | 143 | @property 144 | def available(self): 145 | """Return the available.""" 146 | attr_available = True if (self.coordinator.data.get(self._imei, {}).get("attrs", {}).get("onlinestatus", "") == "在线" ) else False 147 | return attr_available 148 | 149 | @property 150 | def state_attributes(self): 151 | attrs = {} 152 | if self.coordinator.data.get(self._imei): 153 | attrs["querytime"] = self.coordinator.data[self._imei]["attrs"]["querytime"] 154 | return attrs 155 | 156 | async def async_turn_on(self, **kwargs): 157 | """Turn switch on.""" 158 | self._doing = True 159 | await self._switch._turn_on(self.entity_description.key) 160 | self._is_on = True 161 | await self.coordinator.async_request_refresh() 162 | 163 | async def async_turn_off(self, **kwargs): 164 | """Turn switch off.""" 165 | self._doing = True 166 | await self._switch._turn_off(self.entity_description.key) 167 | self._is_on = False 168 | await self.coordinator.async_request_refresh() 169 | 170 | async def async_added_to_hass(self): 171 | """Connect to dispatcher listening for entity data notifications.""" 172 | self.async_on_remove( 173 | self.coordinator.async_add_listener(self.async_write_ha_state) 174 | ) 175 | 176 | async def async_update(self): 177 | """Update entity.""" 178 | _LOGGER.debug("刷新switch数据") 179 | # await self.coordinator.async_request_refresh() 180 | if self._doing == False: 181 | if self._webhost == "hellobike.com": 182 | if self.entity_description.key == "defence": 183 | _LOGGER.debug("defence: %s", self.coordinator.data[self._imei]) 184 | self._is_on = self.coordinator.data[self._imei]["attrs"].get("defence")== "已设防" 185 | elif self.entity_description.key == "defencemod": 186 | _LOGGER.debug("open_lock: %s", self.coordinator.data[self._imei]) 187 | self._is_on = self.coordinator.data[self._imei]["attrs"].get("acc")== "已" 188 | 189 | elif self._webhost == "tuqiang123.com": 190 | if self.entity_description.key == "defence": 191 | _LOGGER.debug("defence: %s", self.coordinator.data[self._imei]) 192 | self._is_on = self.coordinator.data[self._imei]["attrs"].get("defence")== "已设防" 193 | elif self.entity_description.key == "defencemode": 194 | _LOGGER.debug("open_lock: %s", self.coordinator.data[self._imei]) 195 | self._is_on = self.coordinator.data[self._imei]["attrs"].get("acc")== "已启动" 196 | 197 | self._doing = False 198 | 199 | -------------------------------------------------------------------------------- /custom_components/cloud_gps/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "title": "云平台GPS", 4 | "step": { 5 | "user": { 6 | "title": "云平台GPS", 7 | "description": "配置完成后请进入选项中启用相关设备。如果您需要配置方面的帮助,请查看此处: https://github.com/dscao/cloud_gps", 8 | "data": { 9 | "name": "名称", 10 | "username": "用户名", 11 | "password": "用户密码", 12 | "webhost": "服务器" 13 | } 14 | } 15 | }, 16 | "error": { 17 | "communication": "用户名、密码可能无效,请检查。" 18 | }, 19 | "abort": { 20 | "single_instance_allowed": "仅允许单个配置.", 21 | "already_configured": "请勿重复配置." 22 | } 23 | }, 24 | "options": { 25 | "step": { 26 | "user":{ 27 | "data": { 28 | "password": "密码或token,当原来的过期或失效时修改。", 29 | "device_imei": "启用的设备唯一编号(imei、mac、id等)", 30 | "attr_show": "属性中显示停车时间等更丰富信息", 31 | "gps_conver": "从平台获取原始数据的座标系", 32 | "update_interval_seconds": "更新间隔时间(10-3600秒),建议设为90", 33 | "sensors": "传感器", 34 | "switchs": "开关", 35 | "buttons": "按钮", 36 | "with_map_card": "实体更多信息对话框显示地图,需已安装百度地图或墨澜地图集成", 37 | "addressapi": "地址获取接口,使用 API 前请您先注册: [高德账号web服务key](https://lbs.amap.com/dev/key) , [百度账号服务端AK](https://lbsyun.baidu.com/apiconsole/key) , [腾讯WebServiceAPI Key](https://lbs.qq.com/dev/console/application/mine) 。", 38 | "api_key": "接口密钥,为空时不获取地址。", 39 | "private_key": "私钥值,数字签名时填写,否则留空。" 40 | }, 41 | "description": "更多设置,座标系:途强/中移行车卫士-WGS84,高德/优驾/哈啰/小牛-国测局。" 42 | } 43 | } 44 | }, 45 | "selector": { 46 | "gps_conver": { 47 | "options": { 48 | "wgs84": "WGS84坐标系", 49 | "gcj02": "国测局坐标(火星坐标,GCJ02)", 50 | "bd09": "百度坐标(BD09)" 51 | } 52 | }, 53 | "with_map_card": { 54 | "options": { 55 | "none": "不显示地图", 56 | "baidu-map": "显示百度地图", 57 | "gaode-map": "显示高德地图" 58 | } 59 | }, 60 | "sensors": { 61 | "options": { 62 | "laststoptime": "上次停止时间", 63 | "parkingtime": "停车时长", 64 | "address": "当前地址", 65 | "speed": "当前速度", 66 | "totalkm": "总里程", 67 | "status": "车辆状态", 68 | "acc": "ACC状态", 69 | "powbattery": "电池电压" 70 | } 71 | }, 72 | "switchs": { 73 | "options": { 74 | "defence": "设防", 75 | "defencemode": "自动设防模式", 76 | "open_lock": "开锁启动" 77 | } 78 | }, 79 | "buttons": { 80 | "options": { 81 | "bell": "鸣笛寻车", 82 | "nowtrack": "立即定位" 83 | } 84 | }, 85 | "addressapi": { 86 | "options": { 87 | "none": "不从api获取地址,使用原平台中的地址信息", 88 | "free": "免key获取百度基础地理信息(稳定性和精确性较差)", 89 | "gaode": "高德地图逆地理接口", 90 | "baidu": "百度地图逆地理接口", 91 | "tencent": "腾讯地图逆地理接口" 92 | } 93 | } 94 | }, 95 | "entity": { 96 | "device_tracker": { 97 | "cloud_device_tracker": { 98 | "state_attributes": { 99 | "speed": { 100 | "name": "当前车速" 101 | }, 102 | "course": { 103 | "name": "行车方向" 104 | }, 105 | "status": { 106 | "name": "当前状态" 107 | }, 108 | "onlinestatus": { 109 | "name": "在线状态" 110 | }, 111 | "device_status": { 112 | "name": "设备状态" 113 | }, 114 | "navistatus": { 115 | "name": "导航状态" 116 | }, 117 | "macaddr": { 118 | "name": "网卡地址" 119 | }, 120 | "expiration": { 121 | "name": "平台到期" 122 | }, 123 | "lastofflinetime": { 124 | "name": "上次离线时间" 125 | }, 126 | "lastonlinetime": { 127 | "name": "上次上线时间" 128 | }, 129 | "last_update": { 130 | "name": "通讯时间" 131 | }, 132 | "querytime": { 133 | "name": "查询时间" 134 | }, 135 | "runorstop": { 136 | "name": "运动状态", 137 | "state": { 138 | "stop": "静止", 139 | "run": "运动" 140 | } 141 | }, 142 | "laststoptime": { 143 | "name": "停车时间" 144 | }, 145 | "parkingtime": { 146 | "name": "停车时长" 147 | }, 148 | "battery": { 149 | "name": "电池电量" 150 | }, 151 | "powbatteryvoltage": { 152 | "name": "外接电压" 153 | }, 154 | "smallBatteryPercent": { 155 | "name": "GPS内置电池电量" 156 | }, 157 | "batteryvoltage": { 158 | "name": "GPS内置电池电压" 159 | }, 160 | "defence": { 161 | "name": "设防状态" 162 | }, 163 | "powerStatus": { 164 | "name": "电源" 165 | }, 166 | "percentageElectricQuantity": { 167 | "name": "电量百分比" 168 | }, 169 | "totalKm": { 170 | "name": "总里程(公里)" 171 | }, 172 | "positionType": { 173 | "name": "定位方式" 174 | }, 175 | "address": { 176 | "name": "地址" 177 | }, 178 | "gps_accuracy": { 179 | "name": "GPS精度" 180 | }, 181 | "latitude": { 182 | "name": "纬度" 183 | }, 184 | "longitude": { 185 | "name": "经度" 186 | }, 187 | "map_gcj_lat": { 188 | "name": "高德地图纬度" 189 | }, 190 | "map_gcj_lng": { 191 | "name": "高德地图经度" 192 | }, 193 | "map_bd_lat": { 194 | "name": "百度地图纬度" 195 | }, 196 | "map_bd_lng": { 197 | "name": "百度地图经度" 198 | }, 199 | "source_type": { 200 | "name": "数据源", 201 | "state": { 202 | "bluetooth_le": "低功耗蓝牙", 203 | "bluetooth": "蓝牙", 204 | "gps": "GPS定位", 205 | "router": "路由器" 206 | } 207 | } 208 | } 209 | } 210 | }, 211 | "sensor": { 212 | "parkingtime": { 213 | "name": "停车时长", 214 | "state_attributes": { 215 | "querytime": { 216 | "name": "查询时间" 217 | } 218 | } 219 | }, 220 | "laststoptime": { 221 | "name": "上次停止时间", 222 | "state_attributes": { 223 | "querytime": { 224 | "name": "查询时间" 225 | } 226 | } 227 | }, 228 | "address": { 229 | "name": "当前地址", 230 | "state_attributes": { 231 | "querytime": { 232 | "name": "查询时间" 233 | } 234 | } 235 | }, 236 | "speed": { 237 | "name": "当前车速", 238 | "state_attributes": { 239 | "querytime": { 240 | "name": "查询时间" 241 | } 242 | } 243 | }, 244 | "totalkm": { 245 | "name": "总里程", 246 | "state_attributes": { 247 | "querytime": { 248 | "name": "查询时间" 249 | } 250 | } 251 | }, 252 | "status": { 253 | "name": "车辆状态", 254 | "state_attributes": { 255 | "querytime": { 256 | "name": "查询时间" 257 | } 258 | } 259 | }, 260 | "acc": { 261 | "name": "ACC状态", 262 | "state_attributes": { 263 | "querytime": { 264 | "name": "查询时间" 265 | } 266 | } 267 | }, 268 | "powbattery": { 269 | "name": "电池电压", 270 | "state_attributes": { 271 | "querytime": { 272 | "name": "查询时间" 273 | } 274 | } 275 | } 276 | }, 277 | "switch": { 278 | "defence": { 279 | "name": "设防", 280 | "state_attributes": { 281 | "querytime": { 282 | "name": "查询时间" 283 | } 284 | } 285 | }, 286 | "defencemode": { 287 | "name": "自动设防模式", 288 | "state_attributes": { 289 | "querytime": { 290 | "name": "查询时间" 291 | } 292 | } 293 | }, 294 | "open_lock": { 295 | "name": "开锁启动", 296 | "state_attributes": { 297 | "querytime": { 298 | "name": "查询时间" 299 | } 300 | } 301 | } 302 | }, 303 | "button": { 304 | "bell": { 305 | "name": "鸣笛寻车", 306 | "state_attributes": { 307 | "querytime": { 308 | "name": "查询时间" 309 | } 310 | } 311 | }, 312 | "nowtrack": { 313 | "name": "立即定位", 314 | "state_attributes": { 315 | "querytime": { 316 | "name": "查询时间" 317 | } 318 | } 319 | } 320 | } 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /custom_components/cloud_gps/tuqiang123_data_fetcher.py: -------------------------------------------------------------------------------- 1 | """ 2 | get info 3 | """ 4 | 5 | import logging 6 | import requests 7 | import re 8 | import asyncio 9 | import json 10 | import time 11 | import datetime 12 | from async_timeout import timeout 13 | from aiohttp.client_exceptions import ClientConnectorError 14 | from homeassistant.helpers.aiohttp_client import async_create_clientsession 15 | from homeassistant.helpers.update_coordinator import UpdateFailed 16 | from urllib3.util.retry import Retry 17 | from requests.adapters import HTTPAdapter 18 | from homeassistant.const import ( 19 | CONF_USERNAME, 20 | CONF_PASSWORD, 21 | CONF_CLIENT_ID, 22 | ) 23 | 24 | from .const import ( 25 | COORDINATOR, 26 | DOMAIN, 27 | CONF_WEB_HOST, 28 | CONF_DEVICE_IMEI, 29 | UNDO_UPDATE_LISTENER, 30 | CONF_ATTR_SHOW, 31 | CONF_UPDATE_INTERVAL, 32 | ) 33 | 34 | _LOGGER = logging.getLogger(__name__) 35 | 36 | TUQIANG_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36' 37 | TUQIANG123_API_HOST = "https://www.tuqiang123.com" # http://www.tuqiangol.com 或者 http://www.tuqiang123.com 38 | 39 | class DataFetcher: 40 | """fetch the cloud gps data""" 41 | 42 | def __init__(self, hass, username, password, device_imei, location_key): 43 | self.hass = hass 44 | self.location_key = location_key 45 | self.username = username 46 | self.password = password 47 | self.device_imei = device_imei 48 | self.session_tuqiang123 = requests.session() 49 | self.userid = None 50 | self.usertype = None 51 | self._lat_old = 0 52 | self._lon_old = 0 53 | self.deviceinfo = {} 54 | self.trackerdata = {} 55 | self.address = {} 56 | self.totalkm = {} 57 | 58 | headers = { 59 | 'User-Agent': TUQIANG_USER_AGENT 60 | } 61 | self.session_tuqiang123.headers.update(headers) 62 | 63 | def _encode(self, code): 64 | en_code = '' 65 | for s in code: 66 | en_code = en_code + str(ord(s)) + '|' 67 | return en_code[:-1] 68 | 69 | def _login(self, username, password): 70 | p_data = { 71 | 'ver': '1', 72 | 'method': 'login', 73 | 'account': username, 74 | 'password': self._encode(password), 75 | 'language': 'zh' 76 | } 77 | url = TUQIANG123_API_HOST + '/api/regdc' 78 | response = self.session_tuqiang123.post(url, data=p_data) 79 | _LOGGER.debug("TUQIANG123_API_HOST cookies: %s", self.session_tuqiang123.cookies) 80 | _LOGGER.debug(response.json()) 81 | if response.json()['code'] == 0: 82 | self._get_userid() 83 | return True 84 | else: 85 | return False 86 | 87 | def _get_userid(self): 88 | url = TUQIANG123_API_HOST + '/customer/getProviderList' 89 | resp = self.session_tuqiang123.post(url, data=None).json() 90 | self.userid = resp['data']['user']['userId'] 91 | self.usertype = resp['data']['user']['type'] 92 | 93 | def _get_device_info(self, imei_sn): 94 | url = TUQIANG123_API_HOST + '/device/list' 95 | p_data = { 96 | 'dateType': 'activation', 97 | 'equipment.userId': self.userid 98 | } 99 | resp = self.session_tuqiang123.post(url, data=p_data) 100 | 101 | return resp.json()['data']['result'][0] 102 | 103 | def _get_device_tracker(self, imei_sn): 104 | url = TUQIANG123_API_HOST + '/console/refresh' 105 | p_data = { 106 | 'choiceUserId': self.userid, 107 | 'normalImeis': str(imei_sn), 108 | 'userType': self.usertype, 109 | 'followImeis': '', 110 | 'userId': self.userid, 111 | 'stock': '2' 112 | } 113 | resp = self.session_tuqiang123.post(url, data=p_data) 114 | return resp.json()['data']['normalList'][0] 115 | 116 | def _get_device_address(self, lat, lng): 117 | url = TUQIANG123_API_HOST + '/getAddress?lat='+str(lat)+'&lng='+str(lng)+'&mapType=baiduMap&poiList=' 118 | resp = self.session_tuqiang123.get(url) 119 | return resp.json()['msg'] 120 | 121 | def time_diff(self, timestamp): 122 | result = datetime.datetime.now() - datetime.datetime.fromtimestamp(timestamp) 123 | hours = int(result.seconds / 3600) 124 | minutes = int(result.seconds % 3600 / 60) 125 | seconds = result.seconds%3600%60 126 | if result.days > 0: 127 | return("{0}天{1}小时{2}分钟".format(result.days,hours,minutes)) 128 | elif hours > 0: 129 | return("{0}小时{1}分钟".format(hours,minutes)) 130 | elif minutes > 0: 131 | return("{0}分钟{1}秒".format(minutes,seconds)) 132 | else: 133 | return("{0}秒".format(seconds)) 134 | 135 | async def get_data(self): 136 | 137 | _LOGGER.debug(self.device_imei) 138 | if self.userid is None or self.usertype is None: 139 | await self.hass.async_add_executor_job(self._login, self.username, self.password) 140 | 141 | for imei in self.device_imei: 142 | _LOGGER.debug("Requests imei: %s", imei) 143 | self.trackerdata[imei] = {} 144 | if not self.deviceinfo.get(imei): 145 | self.deviceinfo[imei] = {} 146 | try: 147 | async with timeout(10): 148 | infodata = await self.hass.async_add_executor_job(self._get_device_info, imei) 149 | except ( 150 | ClientConnectorError 151 | ) as error: 152 | raise 153 | 154 | _LOGGER.debug("result infodata: %s", infodata) 155 | 156 | if infodata: 157 | self.deviceinfo[imei] =infodata 158 | self.deviceinfo[imei]["device_model"] = "途强在线GPS" 159 | self.deviceinfo[imei]["sw_version"] = infodata["mcType"] 160 | self.deviceinfo[imei]["expiration"] = infodata["expiration"] 161 | 162 | try: 163 | async with timeout(10): 164 | data = await self.hass.async_add_executor_job(self._get_device_tracker, imei) 165 | except ClientConnectorError as error: 166 | _LOGGER.error("连接错误: %s", error) 167 | except asyncio.TimeoutError: 168 | _LOGGER.error("获取数据超时 (10秒)") 169 | except Exception as e: 170 | await self.hass.async_add_executor_job(self._login, self.username, self.password) 171 | raise UpdateFailed(e) 172 | finally: 173 | _LOGGER.debug("最终数据结果: %s", data) 174 | 175 | if data: 176 | querytime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 177 | updatetime = data["hbTime"] 178 | imei = data["imei"] 179 | 180 | direction = data["direction"] 181 | speed = data.get("speed",0) 182 | gpssignal = data.get("gPSSignal", 0) 183 | 184 | onlinestatus = "在线" 185 | status = "停车" 186 | 187 | if data['acc'] == "1": 188 | acc = "钥匙启动" 189 | status = "钥匙启动" 190 | else: 191 | acc = "钥匙关闭" 192 | 193 | thislat = float(data["lat"]) 194 | thislon = float(data["lng"]) 195 | 196 | if data["status"] == "STATIC": 197 | runorstop = "静止" 198 | speed = 0 199 | parkingtime = data["statusStr"] 200 | statustime = data["statusStr"] 201 | elif data["status"] == "MOVE": 202 | runorstop = "运动" 203 | speed = float(data.get("speed",0)) 204 | parkingtime = "" 205 | statustime = data["statusStr"] 206 | status = "行驶" 207 | elif data["status"] == "OFFLINE": 208 | runorstop = "离线" 209 | onlinestatus = "离线" 210 | status = "离线" 211 | speed = 0 212 | parkingtime = data.get("statusAbstract") 213 | statustime = data["statusStr"] 214 | else: 215 | runorstop = "未知" 216 | speed = 0 217 | parkingtime = "" 218 | statustime = "" 219 | 220 | if data.get("powerStatus") == "1": 221 | powerStatus = "已接通" 222 | else: 223 | powerStatus = "已断开" 224 | status = "外电已断开" 225 | 226 | voltage = "0" if data["voltage"]=="" else data["voltage"] 227 | laststoptime = data["gpsTime"] 228 | positionType = data["positionType"] if speed==0 else "" 229 | 230 | if self._lat_old != thislat or self._lon_old != thislon: 231 | self.address[imei] = await self.hass.async_add_executor_job(self._get_device_address, thislat, thislon) 232 | self.totalkm[imei] = data["totalKm"] 233 | self._lat_old = thislat 234 | self._lon_old = thislon 235 | 236 | address = self.address[imei] 237 | totalKm = self.totalkm[imei] 238 | 239 | attrs ={ 240 | "course":direction, 241 | "speed":speed, 242 | "gpssignal": gpssignal, 243 | "querytime":querytime, 244 | "laststoptime":laststoptime, 245 | "last_update":updatetime, 246 | "runorstop":runorstop, 247 | "onlinestatus": onlinestatus, 248 | "acc":acc, 249 | "powerStatus":powerStatus, 250 | "parkingtime":parkingtime, 251 | "address":address, 252 | "powbatteryvoltage":voltage, 253 | "totalKm":totalKm, 254 | "positionType":positionType, 255 | "statustime": statustime 256 | } 257 | 258 | self.trackerdata[imei] = {"location_key":self.location_key+imei,"deviceinfo":self.deviceinfo[imei],"thislat":thislat,"thislon":thislon,"imei":imei,"status":status,"attrs":attrs} 259 | 260 | return self.trackerdata 261 | 262 | 263 | class GetDataError(Exception): 264 | """request error or response data is unexpected""" 265 | 266 | 267 | class DataButton: 268 | 269 | def __init__(self, hass, username, password, device_imei): 270 | self.hass = hass 271 | self._username = username 272 | self._password = password 273 | self.device_imei = device_imei 274 | self.session_tuqiang123 = requests.session() 275 | self.userid = None 276 | self.usertype = None 277 | 278 | headers = { 279 | 'User-Agent': TUQIANG_USER_AGENT 280 | } 281 | self.session_tuqiang123.headers.update(headers) 282 | 283 | def _encode(self, code): 284 | en_code = '' 285 | for s in code: 286 | en_code = en_code + str(ord(s)) + '|' 287 | return en_code[:-1] 288 | 289 | def _login(self, username, password): 290 | p_data = { 291 | 'ver': '1', 292 | 'method': 'login', 293 | 'account': username, 294 | 'password': self._encode(password), 295 | 'language': 'zh' 296 | } 297 | url = TUQIANG123_API_HOST + '/api/regdc' 298 | response = self.session_tuqiang123.post(url, data=p_data) 299 | _LOGGER.debug("TUQIANG123_API_HOST cookies: %s", self.session_tuqiang123.cookies) 300 | _LOGGER.debug(response.json()) 301 | if response.json()['code'] == 0: 302 | self._get_userid() 303 | return True 304 | else: 305 | return False 306 | 307 | def _get_userid(self): 308 | url = TUQIANG123_API_HOST + '/customer/getProviderList' 309 | resp = self.session_tuqiang123.post(url, data=None).json() 310 | self.userid = resp['data']['user']['userId'] 311 | self.usertype = resp['data']['user']['type'] 312 | 313 | def _do_action(self, action): 314 | url = TUQIANG123_API_HOST + '/device/sendIns' 315 | p_data = { 316 | 'imei': self.device_imei, 317 | 'orderContent': 'GPSON#', 318 | 'instructionId': 111845, 319 | 'instructionName': action, 320 | 'instructionPwd': '', 321 | 'isUsePwd': 0, 322 | 'isOffLine': 1 323 | } 324 | resp = self.session_tuqiang123.post(url, data=p_data) 325 | return resp.json() 326 | 327 | async def _action(self, action): 328 | 329 | if self.userid is None or self.usertype is None: 330 | await self.hass.async_add_executor_job(self._login, self._username, self._password) 331 | 332 | resp = await self.hass.async_add_executor_job(self._do_action, action) 333 | _LOGGER.debug(resp) 334 | state = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 335 | return state 336 | 337 | 338 | class DataSwitch: 339 | 340 | def __init__(self, hass, username, password, device_imei): 341 | self.hass = hass 342 | self._username = username 343 | self._password = password 344 | self.device_imei = device_imei 345 | self.session_tuqiang123 = requests.session() 346 | self.userid = None 347 | self.usertype = None 348 | 349 | headers = { 350 | 'User-Agent': TUQIANG_USER_AGENT 351 | } 352 | self.session_tuqiang123.headers.update(headers) 353 | 354 | def _encode(self, code): 355 | en_code = '' 356 | for s in code: 357 | en_code = en_code + str(ord(s)) + '|' 358 | return en_code[:-1] 359 | 360 | def _login(self, username, password): 361 | p_data = { 362 | 'ver': '1', 363 | 'method': 'login', 364 | 'account': username, 365 | 'password': self._encode(password), 366 | 'language': 'zh' 367 | } 368 | url = TUQIANG123_API_HOST + '/api/regdc' 369 | response = self.session_tuqiang123.post(url, data=p_data) 370 | _LOGGER.debug("TUQIANG123_API_HOST cookies: %s", self.session_tuqiang123.cookies) 371 | _LOGGER.debug(response.json()) 372 | if response.json()['code'] == 0: 373 | self._get_userid() 374 | return True 375 | else: 376 | return False 377 | 378 | def _get_userid(self): 379 | url = TUQIANG123_API_HOST + '/customer/getProviderList' 380 | resp = self.session_tuqiang123.post(url, data=None).json() 381 | self.userid = resp['data']['user']['userId'] 382 | self.usertype = resp['data']['user']['type'] 383 | 384 | def _do_action(self, url, body): 385 | url = url 386 | p_data = body 387 | resp = self.session_tuqiang123.post(url, data=p_data) 388 | return resp.json() 389 | 390 | async def _turn_on(self, action): 391 | 392 | if self.userid is None or self.usertype is None: 393 | await self.hass.async_add_executor_job(self._login, self._username, self._password) 394 | 395 | if action == "defence": 396 | url = TUQIANG123_API_HOST + '/device/sendIns' 397 | json_body = { 398 | 'imei': self.device_imei, 399 | 'orderContent': '111#', 400 | 'instructionId': 97, 401 | 'instructionName': "设防", 402 | 'instructionPwd': '', 403 | 'isUsePwd': 0, 404 | 'isOffLine': 1 405 | } 406 | resp = await self.hass.async_add_executor_job(self._do_action, url, json_body) 407 | _LOGGER.debug("Requests remaining: %s", url) 408 | _LOGGER.debug(resp) 409 | elif action == "defencemode": 410 | url = TUQIANG123_API_HOST + '/device/sendIns' 411 | json_body = { 412 | 'imei': self.device_imei, 413 | 'orderContent': 'DEFMODE,{0}#', 414 | 'instructionId': 98, 415 | 'instructionName': "设防模式", 416 | 'param': '22342,0', 417 | 'instructionPwd': '', 418 | 'isUsePwd': 0, 419 | 'isOffLine': 1 420 | } 421 | resp = await self.hass.async_add_executor_job(self._do_action, url, json_body) 422 | _LOGGER.debug("Requests remaining: %s", url) 423 | _LOGGER.debug(resp) 424 | 425 | 426 | async def _turn_off(self, action): 427 | 428 | if self.userid is None or self.usertype is None: 429 | await self.hass.async_add_executor_job(self._login, self._username, self._password) 430 | 431 | if action == "defence": 432 | url = TUQIANG123_API_HOST + '/device/sendIns' 433 | json_body = { 434 | 'imei': self.device_imei, 435 | 'orderContent': '000#', 436 | 'instructionId': 118, 437 | 'instructionName': "撤防", 438 | 'instructionPwd': '', 439 | 'isUsePwd': 0, 440 | 'isOffLine': 1 441 | } 442 | resp = await self.hass.async_add_executor_job(self._do_action, url, json_body) 443 | _LOGGER.debug("Requests remaining: %s", url) 444 | _LOGGER.debug(resp.text()) 445 | 446 | elif action == "defencemode": 447 | url = TUQIANG123_API_HOST + '/device/sendIns' 448 | json_body = { 449 | 'imei': self.device_imei, 450 | 'orderContent': 'DEFMODE,{0}#', 451 | 'instructionId': 98, 452 | 'instructionName': "设防模式", 453 | 'param': '22342,1', 454 | 'instructionPwd': '', 455 | 'isUsePwd': 0, 456 | 'isOffLine': 1 457 | } 458 | resp = await self.hass.async_add_executor_job(self._do_action, url, json_body) 459 | _LOGGER.debug("Requests remaining: %s", url) 460 | _LOGGER.debug(resp) -------------------------------------------------------------------------------- /custom_components/cloud_gps/tuqiangnet_data_fetcher.py: -------------------------------------------------------------------------------- 1 | """ 2 | get info 3 | """ 4 | 5 | import logging 6 | import requests 7 | import re 8 | import asyncio 9 | import json 10 | import time 11 | import datetime 12 | from async_timeout import timeout 13 | from aiohttp.client_exceptions import ClientConnectorError 14 | from homeassistant.helpers.aiohttp_client import async_create_clientsession 15 | from homeassistant.helpers.update_coordinator import UpdateFailed 16 | from urllib3.util.retry import Retry 17 | from requests.adapters import HTTPAdapter 18 | from homeassistant.const import ( 19 | CONF_USERNAME, 20 | CONF_PASSWORD, 21 | CONF_CLIENT_ID, 22 | ) 23 | 24 | from .const import ( 25 | COORDINATOR, 26 | DOMAIN, 27 | CONF_WEB_HOST, 28 | CONF_DEVICE_IMEI, 29 | UNDO_UPDATE_LISTENER, 30 | CONF_ATTR_SHOW, 31 | CONF_UPDATE_INTERVAL, 32 | ) 33 | 34 | _LOGGER = logging.getLogger(__name__) 35 | 36 | TUQIANGNET_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36' 37 | TUQIANGNET_API_HOST = "http://www.tuqiang.net" 38 | 39 | class DataFetcher: 40 | """fetch the cloud gps data""" 41 | 42 | def __init__(self, hass, username, password, device_imei, location_key): 43 | self.hass = hass 44 | self.location_key = location_key 45 | self.username = username 46 | self.password = password 47 | self.device_imei = device_imei 48 | self.session_tuqiangnet = requests.session() 49 | self.cloudpgs_token = None 50 | self._lat_old = 0 51 | self._lon_old = 0 52 | self.deviceinfo = {} 53 | self.trackerdata = {} 54 | self.address = {} 55 | self.totalkm = {} 56 | 57 | headers = { 58 | 'User-Agent': TUQIANGNET_USER_AGENT 59 | } 60 | self.session_tuqiangnet.headers.update(headers) 61 | 62 | def _login(self, username, password): 63 | p_data = { 64 | 'timeZone': '28800', 65 | 'token': '', 66 | 'userName': username, 67 | 'password': password, 68 | 'lang': 'zh' 69 | } 70 | url = TUQIANGNET_API_HOST + '/loginVerification' 71 | response = self.session_tuqiangnet.post(url, data=p_data) 72 | _LOGGER.debug("TUQIANGNET_API_HOST cookies: %s", self.session_tuqiangnet.cookies) 73 | _LOGGER.debug(response.json()) 74 | if response.json()['code'] == 0: 75 | self.cloudpgs_token = response.json()["data"]["token"] 76 | return True 77 | else: 78 | return False 79 | 80 | def _get_device_info(self, imei_sn): 81 | url = TUQIANGNET_API_HOST + '/device/getDeviceList' 82 | p_data = { 83 | "imeis": imei_sn, 84 | "token": self.cloudpgs_token 85 | } 86 | resp = self.session_tuqiangnet.post(url, data=p_data) 87 | return resp.json()['data'][0] 88 | 89 | def _get_device_tracker(self, imei_sn): 90 | url = TUQIANGNET_API_HOST + '/redis/getGps' 91 | p_data = { 92 | "imei": imei_sn, 93 | "token": self.cloudpgs_token 94 | } 95 | resp = self.session_tuqiangnet.post(url, data=p_data) 96 | return resp.json()['data'] 97 | 98 | def _get_device_totalMileage(self, imei_sn): 99 | url = TUQIANGNET_API_HOST + '/redis/getDeviceOther' 100 | p_data = { 101 | "imei": imei_sn, 102 | "token": self.cloudpgs_token 103 | } 104 | resp = self.session_tuqiangnet.post(url, data=p_data) 105 | _LOGGER.debug("result totalMileage: %s", resp.json()) 106 | return round(float(resp.json()['data']['totalMileage'])/1000, 2) if resp.json()['data'].get('totalMileage')!= None else 0 107 | 108 | 109 | def _get_device_address(self, lat, lng): 110 | url = TUQIANGNET_API_HOST + '/comm/getGpsAddr' 111 | p_data = { 112 | "lat": lat, 113 | "lon": lng, 114 | "token": self.cloudpgs_token 115 | } 116 | resp = self.session_tuqiangnet.post(url, data=p_data) 117 | return resp.json()["data"] 118 | 119 | def time_diff(self, timestamp): 120 | result = datetime.datetime.now() - datetime.datetime.fromtimestamp(timestamp) 121 | hours = int(result.seconds / 3600) 122 | minutes = int(result.seconds % 3600 / 60) 123 | seconds = result.seconds%3600%60 124 | if result.days > 0: 125 | return("{0}天{1}小时{2}分钟".format(result.days,hours,minutes)) 126 | elif hours > 0: 127 | return("{0}小时{1}分钟".format(hours,minutes)) 128 | elif minutes > 0: 129 | return("{0}分钟{1}秒".format(minutes,seconds)) 130 | else: 131 | return("{0}秒".format(seconds)) 132 | 133 | async def get_data(self): 134 | 135 | if self.cloudpgs_token is None: 136 | await self.hass.async_add_executor_job(self._login, self.username, self.password) 137 | _LOGGER.debug(self.device_imei) 138 | for imei in self.device_imei: 139 | _LOGGER.debug("Requests imei: %s", imei) 140 | self.trackerdata[imei] = {} 141 | if not self.deviceinfo.get(imei): 142 | self.deviceinfo[imei] = {} 143 | try: 144 | async with timeout(10): 145 | infodata = await self.hass.async_add_executor_job(self._get_device_info, imei) 146 | 147 | except ClientConnectorError as error: 148 | _LOGGER.error("连接错误: %s", error) 149 | except asyncio.TimeoutError: 150 | _LOGGER.error("获取数据超时 (10秒)") 151 | except Exception as e: 152 | _LOGGER.error("未知错误: %s", repr(e)) 153 | finally: 154 | _LOGGER.debug("最终数据结果: %s", infodata) 155 | 156 | 157 | if infodata: 158 | self.deviceinfo[imei] =infodata 159 | self.deviceinfo[imei]["device_model"] = "途强物联GPS" 160 | self.deviceinfo[imei]["sw_version"] = infodata["deviceModel"] 161 | self.deviceinfo[imei]["expiration"] = infodata["expirationTime"] 162 | 163 | try: 164 | async with timeout(10): 165 | data = await self.hass.async_add_executor_job(self._get_device_tracker, imei) 166 | except ClientConnectorError as error: 167 | _LOGGER.error("连接错误: %s", error) 168 | except asyncio.TimeoutError: 169 | _LOGGER.error("获取数据超时 (10秒)") 170 | except Exception as e: 171 | await self.hass.async_add_executor_job(self._login, self.username, self.password) 172 | raise UpdateFailed(e) 173 | finally: 174 | _LOGGER.debug("最终数据结果: %s", data) 175 | 176 | if data: 177 | querytime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 178 | updatetime = data["hbTime"] 179 | direction = data["direction"] 180 | speed = float(data.get("speed",0)) 181 | 182 | status = "停车" 183 | 184 | if data['acc'] == "1": 185 | acc = "钥匙开启" 186 | status = "钥匙开启" 187 | else: 188 | acc = "钥匙关闭" 189 | 190 | thislat = float(data["latitude"]) 191 | thislon = float(data["longitude"]) 192 | voltage = data['extVol'] 193 | percentageElectricQuantity = data['percentageElectricQuantity'] 194 | laststoptime = data["statusUpdateTime"] 195 | if speed == 0: 196 | parkingtime = self.time_diff(int(time.mktime(time.strptime(laststoptime, "%Y-%m-%d %H:%M:%S")))) 197 | runorstop = "静止" 198 | else: 199 | parkingtime = "" 200 | runorstop = "运动" 201 | status = "行驶" 202 | positionType = "GPS" if data["locType"] == "0" else "基站定位" 203 | if data['status'] == "2": 204 | onlinestatus = "在线" 205 | elif data['status'] == "3": 206 | onlinestatus = "在线" 207 | else: 208 | status = "离线" 209 | onlinestatus = "离线" 210 | 211 | if data.get("oilState") == 1: 212 | powerStatus = "已接通" 213 | else: 214 | powerStatus = "已断开" 215 | status = "外电已断开" 216 | 217 | if self._lat_old != thislat or self._lon_old != thislon: 218 | self.address[imei] = await self.hass.async_add_executor_job(self._get_device_address, thislat, thislon) 219 | self.totalkm[imei] = await self.hass.async_add_executor_job(self._get_device_totalMileage, imei) 220 | self._lat_old = thislat 221 | self._lon_old = thislon 222 | 223 | address = self.address[imei] 224 | totalKm = self.totalkm[imei] 225 | 226 | attrs = { 227 | "course":direction, 228 | "speed":speed, 229 | "querytime":querytime, 230 | "laststoptime":laststoptime, 231 | "last_update":updatetime, 232 | "runorstop":runorstop, 233 | "onlinestatus": onlinestatus, 234 | "acc":acc, 235 | "powerStatus":powerStatus, 236 | "parkingtime":parkingtime, 237 | "address":address, 238 | "powbatteryvoltage":voltage, 239 | "percentageElectricQuantity": percentageElectricQuantity, 240 | "totalKm":totalKm, 241 | "positionType":positionType 242 | } 243 | 244 | self.trackerdata[imei] = {"location_key":self.location_key+str(imei),"deviceinfo":self.deviceinfo[imei],"thislat":thislat,"thislon":thislon,"imei":imei,"status":status,"attrs":attrs} 245 | 246 | return self.trackerdata 247 | 248 | 249 | class GetDataError(Exception): 250 | """request error or response data is unexpected""" 251 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloud_gps", 3 | "render_readme": true 4 | } 5 | --------------------------------------------------------------------------------