-
Notifications
You must be signed in to change notification settings - Fork 1
/
plugin.py
executable file
·813 lines (709 loc) · 35.2 KB
/
plugin.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
# Copyright (C) 2021 John de Rooij
#
# This software is licensed as described in the file LICENSE, which
# you should have received as part of this distribution.
#
# Domoticz-Toyota-Plugin ( https://github.com/joro75/Domoticz-Toyota-Plugin )
#
# CodingGuidelines 2020-04-11
# pylint:disable=line-too-long
"""
<plugin key="Toyota" name="Toyota" author="joro75" version="0.9.3"
externallink="https://github.com/joro75/Domoticz-Toyota-Plugin">
<description>
<h2>Domoticz Toyota Plugin 0.9.3</h2>
<p>
A Domoticz plugin that provides devices for a Toyota car with connected services.
</p>
<p>
It is using the same API that is used by the Toyota MyT connected services.
This API is however only useable for cars that are purchased in Europe.
For more information on Toyota MyT see the
<a href="https://www.toyota.at/owners/myt-and-multimedia">Austrian</a>,
<a href="https://nl.toyota.be/naverkoop/connected-services/myt">Belgian</a>,
<a href="https://www.toyota.co.uk/owners/servicing-and-aftercare/my-toyota/myt-and-connected-services">British</a>,
<a href="https://www.toyota.dk/toyota-ejere/din-toyota/myt-connected-services">Danish</a>,
<a href="https://www.toyota.nl/toyota-rijders/connected-services1/myt">Dutch</a>,
<a href="https://www.toyota-europe.com/service-and-accessories/my-toyota/myt">European</a>,
<a href="https://www.toyota.fr/ma-toyota/application-myt">French</a>,
<a href="https://www.toyota.de/service_und_zubehoer/myt">German</a>,
<a href="https://www.toyota.it/clienti/multimedia/myt-servizi-connessi">Italian</a>,
<a href="https://www.toyota.es/servicios-conectados-myt">Spanish</a> or
<a href="https://fr.toyota.ch/owners/myt-app-multimedia">Swiss</a> website.
</p>
<p>
The Toyota car should first be made available in the MyT connected services,
after which this plugin can retrieve the information, which is then provided as
several devices in Domoticz.
</p>
<h3>Devices</h3>
<ul style="list-style-type:square">
<li>Mileage - Shows the daily and total mileage of the car</li>
<li>Fuel level - Shows the actual fuel level percentage</li>
<li>Distance to home - Shows the distance between the car and home</li>
<li>Locked - Shows if the car is locked or unlocked</li>
<li>Parking location - Shows the address of the parking location of the car</li>
<li>Consumed fuel - Shows the average consumed fuel in l/100 km</li>
<li>Accelerations - Shows the number of hard accelerations</li>
<li>Brakes - Shows the number of hard brakes</li>
<li>Duration - Shows the total driving duration in seconds</li>
<li>Idle - Shows the total standstill duration in seconds</li>
</ul>
<h3>Configuration</h3>
<ul style="list-style-type:square">
<li>Username - The username that is also used to login in the myT application.</li>
<li>Password - The password that is also used to login in the myT application.</li>
<li>Car - An identifier for the car for which the data should be retrieved,
if multiple cars are present in the myT application.
It can be a part of the VIN number, alias, licenseplate or the model.</li>
</ul>
</description>
<params>
<param field="Username" label="Username" width="200px" required="true"/>
<param field="Password" label="Password" width="200px" required="true" password="true"/>
<!-- Mode1 has been used in the past for the Locale. Not reusing it yet. -->
<param field="Mode2" label="Car" width="200px" required="false" />
</params>
</plugin>
"""
# pylint:enable=line-too-long
import sys
from abc import ABC, abstractmethod
import asyncio
import datetime
from typing import Any, Union, List, Tuple, Optional, Dict
import arrow # pylint:disable=import-error
MINIMUM_PYTHON_VERSION = (3, 8)
DO_DOMOTICZ_DEBUGGING: bool = False
MINIMUM_MYTOYOTA_VERSION: str = '0.9.3'
MINIMUM_GEOPY_VERSION: str = '2.3.0'
NOMINATIM_USER_AGENT = 'Domoticz-Toyota-Plugin'
_importErrors = [] # pylint:disable=invalid-name
try:
import Domoticz # type: ignore
except (ModuleNotFoundError, ImportError):
_importErrors += [('The Python Domoticz library is not installed. '
'This plugin can only be used in Domoticz. '
'Check your Domoticz installation')]
# Fool mypy and pylint that these types are coming from Domoticz
try:
from Domoticz import Parameters, Devices, Settings, Images
except (ModuleNotFoundError, ImportError):
pass
try:
import setuptools # type: ignore
Version = setuptools.distutils.version.LooseVersion
except (ModuleNotFoundError, ImportError):
_importErrors += ['The python setuptools library is not installed.']
try:
import mytoyota # type: ignore
try:
mytoyota_version = Version(mytoyota.__version__)
if mytoyota_version < Version(MINIMUM_MYTOYOTA_VERSION):
_importErrors += ['The mytoyota version is too old, an update is needed.']
del mytoyota
del sys.modules['mytoyota']
except AttributeError:
_importErrors += ['The mytoyota version is too old, an update is needed.']
if 'mytoyota' in sys.modules:
from mytoyota import MyT # type: ignore
import mytoyota.exceptions # type: ignore
import mytoyota.models.vehicle # type: ignore
except (ModuleNotFoundError, ImportError):
_importErrors += ['The Python mytoyota library is not installed.']
try:
import geopy.distance # type: ignore
geopy_version = Version(geopy.__version__)
if geopy_version < Version(MINIMUM_GEOPY_VERSION):
_importErrors += ['The geopy version is too old, an update is needed.']
del geopy
del sys.modules['geopy']
if 'geopy' in sys.modules:
from geopy.geocoders import Nominatim # type: ignore
except (ModuleNotFoundError, ImportError):
_importErrors += ['The python geopy library is not installed.']
UNIT_MILEAGE_INDEX: int = 1
UNIT_FUEL_INDEX: int = 2
UNIT_DISTANCE_INDEX: int = 3
UNIT_CAR_LOCKED_INDEX: int = 4
UNIT_PARKING_LOCATION_INDEX: int = 5
UNIT_CONSUMED_FUEL_INDEX: int = 6
UNIT_ACCELERATIONS_INDEX: int = 7
UNIT_BRAKES_INDEX: int = 8
UNIT_DURATION_INDEX: int = 9
UNIT_IDLE_INDEX: int = 10
class ReducedHeartBeat(ABC):
"""Helper class that only calls the update of the devices every ... heartbeat."""
_heartbeat_interval: int = 10
def __init__(self) -> None:
super().__init__()
self._heartbeat_count = self._heartbeat_interval
def onHeartbeat(self) -> None: # pylint:disable=invalid-name
"""Callback from Domoticz that the plugin can perform some work."""
self._heartbeat_count += 1
if self._heartbeat_count > self._heartbeat_interval:
self._heartbeat_count = 0
self.update_devices()
@abstractmethod
def update_devices(self) -> None:
"""Retrieve the status of the device and update the Domoticz devices."""
return
class ToyotaMyTConnector():
"""Provide a connection to the Toyota MyT service."""
def __init__(self) -> None:
super().__init__()
self._logged_on = False
self._client: MyT = None
self._car: Optional[Dict[str, Any]] = None
def _lookup_car(self, cars: Optional[List[Dict[str, Any]]], # pylint:disable=no-self-use
identifier: str) -> Optional[Dict[str, Any]]:
"""Find and eturn the first car from cars that confirms to the passed identifier."""
if not cars is None and identifier:
car_id = identifier.upper().strip()
for car in cars:
if car_id in car.get('alias', '').upper():
return car
if car_id in car.get('licensePlate', '').upper():
return car
if car_id in car.get('vin', '').upper():
return car
if car_id in car.get('modelName', '').upper():
return car
return None
def _connect_to_myt(self) -> None:
"""Connect to the Toyota MyT servers."""
self._logged_on = False
cars: Optional[List[Any]] = None
try:
self._client = MyT(username=Parameters['Username'],
password=Parameters['Password'])
asyncio.run(self._client.login())
cars = asyncio.run(self._client.get_vehicles())
self._logged_on = True
except mytoyota.exceptions.ToyotaLoginError as ex:
Domoticz.Error(f'Login Error: {ex}')
except mytoyota.exceptions.ToyotaInvalidUsername as ex:
Domoticz.Error(f'Invalid username: {ex}')
if self._logged_on:
Domoticz.Log('Succesfully logged on')
self._car = self._lookup_car(cars, Parameters['Mode2'])
if self._car is None:
self._car = self._lookup_car(cars, Parameters['Name'])
if self._car is None:
Domoticz.Error('Could not find the desired car in the MyT information')
else:
Domoticz.Error('Logon failed')
def _ensure_connected(self) -> bool:
"""
Check and return if a connection to Toyota MyT servers is present,
also trying to connect.
"""
if not self._is_connected():
self._connect_to_myt()
return self._is_connected()
def _is_connected(self) -> bool:
"""Check and return if a connection to Toyota MyT servers is present."""
connected = False
if self._logged_on:
if self._car:
connected = True
return connected
def retrieve_vehicle_status(self) -> Union[Any, None]:
"""Retrieve and return the status information of the vehicle."""
vehicle = None
if self._ensure_connected():
Domoticz.Log('Updating vehicle status')
try:
vehicle = asyncio.run(self._client.get_vehicle_status(self._car))
except mytoyota.exceptions.ToyotaInternalError:
pass
if vehicle is None:
Domoticz.Error('Vehicle status could not be retrieved')
return vehicle
def retrieve_statistics(self) -> Optional[Dict[str, str]]:
"""Retrieve the statistics of today"""
statistics = None
if self._ensure_connected():
Domoticz.Log('Retrieving vehicle statistics')
try:
vin: str = self._car.get('vin', '') if self._car else ''
statistics = asyncio.run(self._client.get_driving_statistics(vin, interval='day'))
except mytoyota.exceptions.ToyotaInternalError:
pass
except TypeError as inst:
Domoticz.Error(f'TypeError exception raised: {inst}')
Domoticz.Dump()
Domoticz.Log('Vehicle statistics received')
stats_today = None
Domoticz.Log('Looking up statistics of today')
if statistics is None:
Domoticz.Error('Vehicle statistics could not be retrieved')
else:
today = datetime.date.today().isoformat()
for record in statistics:
bucket_data = record.get('bucket', None)
date = bucket_data.get('date', '') if bucket_data else ''
if date == today:
stats_today = record.get('data', None)
break
return stats_today
def disconnect(self) -> None:
"""Disconnect from the Toyota MyT servers."""
self._client = None
class DomoticzDevice(ABC): # pylint:disable=too-few-public-methods
"""Representation of a generic updateable Domoticz devices."""
def __init__(self, unit_index: int) -> None:
super().__init__()
self._unit_index = unit_index
self._last_update = datetime.datetime.now()
self._update_interval = 6 * 3600
self._do_first_update = True
def exists(self) -> bool:
"""Check if the Domoticz device is present and existing."""
return (self._unit_index in Devices) and (Devices[self._unit_index])
def did_update(self) -> None:
"""Remember that an update of the device is done."""
self._last_update = datetime.datetime.now()
self._do_first_update = False
def requires_update(self) -> bool:
"""Determine if an update of the device is needed."""
diff = datetime.datetime.now() - self._last_update
return (diff.seconds > self._update_interval) or self._do_first_update
class ToyotaDomoticzDevice(DomoticzDevice):
"""
A generic updateable Domoticz device, to represent information from
a Toyota MyT connected services car.
"""
@abstractmethod
def create(self, vehicle_status) -> None:
"""Check if the device is present in Domoticz, and otherwise create it."""
return
def update(self, vehicle_status) -> None: # pylint:disable=no-self-use,unused-argument
"""
Determine the actual value of the instrument and
update the device in Domoticz.
"""
return
def update_statistics(self, statistics) -> None: # pylint:disable=no-self-use,unused-argument
"""
Determine the actual value of the statistic of
today and update the device in Domoticz.
"""
return
class MileageToyotaDevice(ToyotaDomoticzDevice):
"""The Domoticz device that shows the mileage."""
def __init__(self) -> None:
super().__init__(UNIT_MILEAGE_INDEX)
self._last_mileage: int = 0
def create(self, vehicle_status) -> None:
"""Check if the device is present in Domoticz, and otherwise create it."""
if vehicle_status:
if not self.exists():
Domoticz.Device(Name='Mileage', Unit=self._unit_index,
TypeName='Counter Incremental', Switchtype=3,
Used=1,
Description='Counter to hold the overall mileage',
Options={'ValueQuantity': 'Distance',
'ValueUnits': 'km',
}
).Create()
# Retrieve the last mileage that is already known in Domoticz
if self.exists():
try:
self._last_mileage = int(Devices[self._unit_index].sValue)
except ValueError:
self._last_mileage = 0
def update(self, vehicle_status) -> None:
"""Determine the actual value of the instrument and update the device in Domoticz."""
if vehicle_status and vehicle_status.dashboard:
if self.exists():
mileage = vehicle_status.dashboard.odometer
diff = mileage - self._last_mileage
if diff > 0 or self.requires_update():
# Mileage can only go up
Devices[self._unit_index].Update(nValue=0, sValue=f'{diff}')
self._last_mileage = mileage
self.did_update()
class FuelToyotaDevice(ToyotaDomoticzDevice):
"""The Domoticz device that shows the fuel level percentage."""
def __init__(self) -> None:
super().__init__(UNIT_FUEL_INDEX)
self._last_fuel: float = 0.0
def create(self, vehicle_status) -> None:
"""Check if the device is present in Domoticz, and otherwise create it."""
if vehicle_status:
if not self.exists():
Domoticz.Image('ToyotaFuelMeter.zip').Create()
Domoticz.Device(Name='Fuel level', Unit=self._unit_index,
TypeName='Percentage',
Used=1,
Description='The filled percentage of the fuel tank',
Image=Images['ToyotaFuelMeter'].ID
).Create()
if self.exists():
try:
self._last_fuel = float(Devices[self._unit_index].sValue)
except ValueError:
self._last_fuel = 0
def update(self, vehicle_status) -> None:
"""Determine the actual value of the instrument and update the device in Domoticz."""
if vehicle_status and vehicle_status.dashboard:
if self.exists():
fuel = vehicle_status.dashboard.fuel_level
if fuel != self._last_fuel or self.requires_update():
Devices[self._unit_index].Update(nValue=int(float(fuel)), sValue=str(fuel))
self._last_fuel = fuel
self.did_update()
class DistanceToyotaDevice(ToyotaDomoticzDevice):
"""The Domoticz device that shows the distance between the parked car and home."""
def __init__(self) -> None:
super().__init__(UNIT_DISTANCE_INDEX)
self._last_coords: Tuple[float, ...] = (0.0, 0.0)
self._coordinates_home: Optional[Tuple[float, ...]] = None
if Settings['Location']:
try:
self._coordinates_home = tuple(float(part) for part in
Settings['Location'].split(';'))
except ValueError:
pass
def create(self, vehicle_status) -> None:
"""Check if the device is present in Domoticz, and otherwise create it."""
if vehicle_status.parkinglocation:
if not self.exists() and 'geopy' in sys.modules:
Domoticz.Device(Name='Distance to home', Unit=self._unit_index,
TypeName='Custom Sensor', Type=243, Subtype=31,
Options={'Custom': '1;km'},
Used=1,
Description='The distance between home and the car'
).Create()
def update(self, vehicle_status) -> None:
"""Determine the actual value of the instrument and update the device in Domoticz."""
if vehicle_status and vehicle_status.parkinglocation:
if self.exists() and 'geopy' in sys.modules:
if not self._coordinates_home is None:
coords_car = (float(vehicle_status.parkinglocation.latitude),
float(vehicle_status.parkinglocation.longitude))
if coords_car != self._last_coords or self.requires_update():
dist = geopy.distance.distance(self._coordinates_home, coords_car).km
# Round it to meters.
dist = round(dist, 3)
Devices[self._unit_index].Update(nValue=0, sValue=f'{dist}')
self._last_coords = coords_car
self.did_update()
class ParkingLocationToyotaDevice(ToyotaDomoticzDevice):
"""The Domoticz device that shows the address of the parking location of the car."""
def __init__(self) -> None:
super().__init__(UNIT_PARKING_LOCATION_INDEX)
self._last_coords: Tuple[str, ...] = ('', '')
def create(self, vehicle_status) -> None:
"""Check if the device is present in Domoticz, and otherwise create it."""
if vehicle_status.parkinglocation:
if not self.exists() and 'geopy' in sys.modules:
Domoticz.Device(Name='Parking location', Unit=self._unit_index,
TypeName='Text', Type=243, Subtype=19,
Used=1,
Description='The address of the parking location of the car'
).Create()
def update(self, vehicle_status) -> None:
"""Determine the actual value of the instrument and update the device in Domoticz."""
if vehicle_status and vehicle_status.parkinglocation:
if self.exists() and 'geopy' in sys.modules:
coords_car = (str(vehicle_status.parkinglocation.latitude),
str(vehicle_status.parkinglocation.longitude))
if coords_car != self._last_coords or self.requires_update():
address = self._lookup_address(coords_car)
Devices[self._unit_index].Update(nValue=0, sValue=f'{address}')
self._last_coords = coords_car
self.did_update()
def _lookup_address(self, coords: Tuple[str, ...]) -> str: # pylint:disable=no-self-use
"""Determines the address of the given coordinates"""
coord_str = ','.join(coordinate.strip().lower() for coordinate in coords[0:2])
geolocator = Nominatim(user_agent=NOMINATIM_USER_AGENT)
location = geolocator.reverse(coord_str)
return (location.address if location else '')
class LockedToyotaDevice(ToyotaDomoticzDevice):
"""The Domoticz device that shows the locked/unlocked status of the car."""
def __init__(self) -> None:
super().__init__(UNIT_CAR_LOCKED_INDEX)
self._last_state = -1
def create(self, vehicle_status) -> None:
"""Check if the device is present in Domoticz, and otherwise create it."""
if vehicle_status:
if not self.exists() and self._has_info(vehicle_status):
Domoticz.Image('ToyotaLocked.zip').Create()
Domoticz.Device(Name='Locked', Unit=self._unit_index,
TypeName='Light/Switch', Type=244, Subtype=73, Switchtype=19,
Used=1,
Description='The locked/unlocked state of the car',
Image=Images['ToyotaLocked'].ID
).Create()
def _get_doors(self, vehicle_status): # pylint:disable=no-self-use
"""Return an array of individual door instances"""
doors = []
if vehicle_status and vehicle_status.sensors and vehicle_status.sensors.doors:
direct = vehicle_status.sensors.doors
try:
doors = [direct.driver_seat, direct.passenger_seat,
direct.leftrear_seat, direct.rightrear_seat,
direct.trunk]
except (AttributeError, TypeError):
pass
return doors
def _has_info(self, vehicle_status) -> bool: # pylint:disable=no-self-use
"""Determine if the information of the locked state is available."""
present = False
for door in self._get_doors(vehicle_status):
present = present or door is not None
return present
def update(self, vehicle_status) -> None:
"""Determine the actual value of the instrument and update the device in Domoticz."""
if self.exists():
locked = True
for door in self._get_doors(vehicle_status):
try:
locked = locked and door.locked if door is not None else True
except AttributeError:
pass
state = 1 if locked else 0
if state != self._last_state or self.requires_update():
Devices[self._unit_index].Update(nValue=state, sValue=str(state))
self._last_state = state
self.did_update()
class ConsumedFuelToyotaDevice(ToyotaDomoticzDevice):
"""The Domoticz device that shows the average consumed fuelage in l/100 km."""
def __init__(self) -> None:
super().__init__(UNIT_CONSUMED_FUEL_INDEX)
self._last_consumed_fuel: float = 0.0
def create(self, vehicle_status) -> None:
"""Check if the device is present in Domoticz, and otherwise create it."""
if vehicle_status:
if not self.exists():
Domoticz.Device(Name='Consumed fuel', Unit=self._unit_index,
TypeName='Counter Incremental', Switchtype=3,
Used=1,
Description='Average consumed fuel in l/100 km',
Options={'ValueQuantity': 'Consumption',
'ValueUnits': 'l/100 km',
}
).Create()
if self.exists():
try:
self._last_consumed_fuel = float(Devices[self._unit_index].sValue)
except ValueError:
self._last_consumed_fuel = 0
def update_statistics(self, statistics) -> None:
"""Determine the actual value of the statistics and update the device in Domoticz."""
if self.exists():
Domoticz.Log(f'{statistics}')
fuel = float(statistics.get('totalFuelConsumedInL', 0.0)) if statistics else 0.0
Domoticz.Log(f'Fuel consumed: {fuel}')
if fuel != self._last_consumed_fuel or self.requires_update():
# Restore the counter to 0
Devices[self._unit_index].Update(nValue=0, sValue=f'-{self._last_consumed_fuel}')
# Set the actual value for today
Devices[self._unit_index].Update(nValue=0, sValue=f'{fuel}')
self._last_consumed_fuel = fuel
self.did_update()
class AccelerationsToyotaDevice(ToyotaDomoticzDevice):
"""The Domoticz device that shows the number of hard accelerations."""
def __init__(self) -> None:
super().__init__(UNIT_ACCELERATIONS_INDEX)
self._last_accelerations: int = 0
def create(self, vehicle_status) -> None:
"""Check if the device is present in Domoticz, and otherwise create it."""
if vehicle_status:
if not self.exists():
Domoticz.Device(Name='Accelerations', Unit=self._unit_index,
TypeName='Counter Incremental', Switchtype=3,
Used=1,
Description='Number of hard accelerations',
Options={'ValueQuantity': 'Count',
'ValueUnits': '',
}
).Create()
if self.exists():
try:
self._last_accelerations = int(Devices[self._unit_index].sValue)
except ValueError:
self._last_accelerations = 0
def update_statistics(self, statistics) -> None:
"""Determine the actual value of the statistics and update the device in Domoticz."""
if self.exists():
accelerations = int(statistics.get('hardAccelerationCount', 0)) if statistics else 0
diff = accelerations - self._last_accelerations
if diff != 0 or self.requires_update():
value = f'{diff if diff > 0 else accelerations}'
Devices[self._unit_index].Update(nValue=0, sValue=value)
self._last_accelerations = accelerations
self.did_update()
class BrakesToyotaDevice(ToyotaDomoticzDevice):
"""The Domoticz device that shows the number of hard brakes."""
def __init__(self) -> None:
super().__init__(UNIT_BRAKES_INDEX)
self._last_brakes: int = 0
def create(self, vehicle_status) -> None:
"""Check if the device is present in Domoticz, and otherwise create it."""
if vehicle_status:
if not self.exists():
Domoticz.Device(Name='Brakes', Unit=self._unit_index,
TypeName='Counter Incremental', Switchtype=3,
Used=1,
Description='Number of hard brakes',
Options={'ValueQuantity': 'Count',
'ValueUnits': '',
}
).Create()
if self.exists():
try:
self._last_brakes = int(Devices[self._unit_index].sValue)
except ValueError:
self._last_brakes = 0
def update_statistics(self, statistics) -> None:
"""Determine the actual value of the statistics and update the device in Domoticz."""
if self.exists():
brakes = int(statistics.get('hardBrakingCount', 0)) if statistics else 0
diff = brakes - self._last_brakes
if diff != 0 or self.requires_update():
value = f'{diff if diff > 0 else brakes}'
Devices[self._unit_index].Update(nValue=0, sValue=value)
self._last_brakes = brakes
self.did_update()
class DurationToyotaDevice(ToyotaDomoticzDevice):
"""The Domoticz device that shows the total driving duration in seconds."""
def __init__(self) -> None:
super().__init__(UNIT_DURATION_INDEX)
self._last_duration: int = 0
def create(self, vehicle_status) -> None:
"""Check if the device is present in Domoticz, and otherwise create it."""
if vehicle_status:
if not self.exists():
Domoticz.Device(Name='Duration', Unit=self._unit_index,
TypeName='Counter Incremental', Switchtype=3,
Used=1,
Description='Total driving duration in seconds',
Options={'ValueQuantity': 'Duration',
'ValueUnits': 'sec',
}
).Create()
if self.exists():
try:
self._last_duration = int(Devices[self._unit_index].sValue)
except ValueError:
self._last_duration = 0
def update_statistics(self, statistics) -> None:
"""Determine the actual value of the statistics and update the device in Domoticz."""
if self.exists():
duration = int(statistics.get('totalDurationInSec', 0)) if statistics else 0
diff = duration - self._last_duration
if diff != 0 or self.requires_update():
value = f'{diff if diff > 0 else duration}'
Devices[self._unit_index].Update(nValue=0, sValue=value)
self._last_duration = duration
self.did_update()
class IdleToyotaDevice(ToyotaDomoticzDevice):
"""The Domoticz device that shows the total standstill duration in seconds."""
def __init__(self) -> None:
super().__init__(UNIT_IDLE_INDEX)
self._last_idle: int = 0
def create(self, vehicle_status) -> None:
"""Check if the device is present in Domoticz, and otherwise create it."""
if vehicle_status:
if not self.exists():
Domoticz.Device(Name='Idle', Unit=self._unit_index,
TypeName='Counter Incremental', Switchtype=3,
Used=1,
Description='Total standstill duration in seconds',
Options={'ValueQuantity': 'Duration',
'ValueUnits': 'sec',
}
).Create()
if self.exists():
try:
self._last_idle = int(Devices[self._unit_index].sValue)
except ValueError:
self._last_idle = 0
def update_statistics(self, statistics) -> None:
"""Determine the actual value of the statistics and update the device in Domoticz."""
if self.exists():
idle = int(statistics.get('idleDurationInSec', 0)) if statistics else 0
diff = idle - self._last_idle
if diff != 0 or self.requires_update():
value = f'{diff if diff > 0 else idle}'
Devices[self._unit_index].Update(nValue=0, sValue=value)
self._last_idle = idle
self.did_update()
class ToyotaPlugin(ReducedHeartBeat, ToyotaMyTConnector):
"""Domoticz plugin function implementation to get information from Toyota MyT."""
def __init__(self) -> None:
super().__init__()
self._devices: List[ToyotaDomoticzDevice] = []
self._now = arrow.now()
def add_devices(self) -> None:
"""Add all the device classes that are part of this plugin."""
self._devices += [MileageToyotaDevice()]
self._devices += [FuelToyotaDevice()]
self._devices += [DistanceToyotaDevice()]
self._devices += [LockedToyotaDevice()]
self._devices += [ParkingLocationToyotaDevice()]
self._devices += [ConsumedFuelToyotaDevice()]
self._devices += [AccelerationsToyotaDevice()]
self._devices += [BrakesToyotaDevice()]
self._devices += [DurationToyotaDevice()]
self._devices += [IdleToyotaDevice()]
def update_devices(self) -> None:
"""Retrieve the status of the vehicle and update the Domoticz devices."""
vehicle_status = self.retrieve_vehicle_status()
if vehicle_status:
for device in self._devices:
device.update(vehicle_status)
statistics = self.retrieve_statistics()
if statistics:
for device in self._devices:
device.update_statistics(statistics)
def create_devices(self) -> None:
"""Create the appropiate devices in Domoticz for the vehicle."""
vehicle_status = self.retrieve_vehicle_status()
if vehicle_status:
for device in self._devices:
device.create(vehicle_status)
_plugin = ToyotaPlugin() if 'mytoyota' in sys.modules else None # pylint:disable=invalid-name
def onStart() -> None: # pylint:disable=invalid-name
"""Callback from Domoticz that the plugin is started."""
if DO_DOMOTICZ_DEBUGGING:
Domoticz.Debugging(1)
dump_config_to_log()
if sys.version_info < MINIMUM_PYTHON_VERSION:
Domoticz.Error(f'Python version {sys.version_info} is not supported,'
f' at least {MINIMUM_PYTHON_VERSION} is required.')
else:
global _importErrors # pylint:disable=invalid-name,global-statement
if _importErrors:
_importErrors += [('Use pip to install required packages: '
'pip3 install -r requirements.txt')]
for err in _importErrors:
Domoticz.Error(err)
elif _plugin:
_plugin.add_devices()
_plugin.create_devices()
def onStop() -> None: # pylint:disable=invalid-name
"""Callback from Domoticz that the plugin is stopped."""
if _plugin:
_plugin.disconnect()
def onHeartbeat() -> None: # pylint:disable=invalid-name
"""Callback from Domoticz that the plugin can perform some work."""
if _plugin:
_plugin.onHeartbeat()
def dump_config_to_log() -> None:
"""Dump the configuration of the plugin to the Domoticz debug log."""
for key in Parameters:
if Parameters[key] != '':
value = '******' if key.lower() in ['username', 'password'] else str(Parameters[key])
Domoticz.Debug(f'\'{key}\': \'{value}\'')
Domoticz.Debug(f'Device count: {str(len(Devices))}')
for key in Devices:
Domoticz.Debug(f'Device: {str(key)} - {str(Devices[key])}')
Domoticz.Debug(f'Device ID: \'{str(Devices[key].ID)}\'')
Domoticz.Debug(f'Device Name: \'{Devices[key].Name}\'')
Domoticz.Debug(f'Device nValue: {str(Devices[key].nValue)}')
Domoticz.Debug(f'Device sValue: \'{Devices[key].sValue}\'')
Domoticz.Debug(f'Device LastLevel: {str(Devices[key].LastLevel)}')