├── 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 | 
32 |
33 |
34 | 
35 |
36 |
37 | 
38 |
39 |
40 | 
41 |
42 |
43 | 
44 |
45 |
46 | 
47 |
48 |
49 | 
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 |
--------------------------------------------------------------------------------