Skip to content

Commit 6e862e9

Browse files
committed
add charge profiles
1 parent 43d7318 commit 6e862e9

File tree

6 files changed

+365
-170
lines changed

6 files changed

+365
-170
lines changed

weconnect/addressable.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
from typing import Callable, NoReturn, Optional, Dict, List, Set, Any, Tuple, Union, Type, TypeVar, Generic
33

44
import logging
5-
from datetime import datetime, timezone
5+
import time as timemodule
6+
from datetime import datetime, timezone, time
67
from enum import Enum, IntEnum, Flag, auto
78

89
from weconnect.util import toBool, imgToASCIIArt, robustTimeParse
@@ -296,6 +297,9 @@ def fromDict(self, fromDict: dict, key: str): # noqa: C901
296297
self.enabled = False
297298
elif issubclass(self.valueType, datetime):
298299
self.setValueWithCarTime(robustTimeParse(fromDict[key]), lastUpdateFromCar=None, fromServer=True)
300+
elif issubclass(self.valueType, time):
301+
parsedtime = timemodule.strptime(fromDict[key], "%H:%M")
302+
self.setValueWithCarTime(time(hour=parsedtime.tm_hour, minute=parsedtime.tm_min), lastUpdateFromCar=None, fromServer=True)
299303
elif issubclass(self.valueType, str):
300304
self.setValueWithCarTime(str(fromDict[key]), lastUpdateFromCar=None, fromServer=True)
301305
else:

weconnect/elements/charging_profiles.py

Lines changed: 180 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
from datetime import datetime
1+
from datetime import datetime, time
22
import logging
33

4-
from weconnect.addressable import AddressableAttribute, AddressableList
4+
from weconnect.addressable import AddressableAttribute, AddressableDict, AddressableObject
5+
from weconnect.elements.enums import UnlockPlugState
56
from weconnect.elements.generic_settings import GenericSettings
7+
from weconnect.elements.timer import Timer
68

79
LOG = logging.getLogger("weconnect")
810

@@ -16,7 +18,7 @@ def __init__(
1618
fromDict=None,
1719
fixAPI=True,
1820
):
19-
self.profiles = AddressableList(localAddress='profiles', parent=self)
21+
self.profiles = AddressableDict(localAddress='profiles', parent=self)
2022
self.timeInCar = AddressableAttribute(localAddress='timeInCar', parent=self, value=None, valueType=datetime)
2123
super().__init__(vehicle=vehicle, parent=parent, statusId=statusId, fromDict=fromDict, fixAPI=fixAPI)
2224

@@ -26,8 +28,15 @@ def update(self, fromDict, ignoreAttributes=None):
2628

2729
if 'value' in fromDict:
2830
if 'profiles' in fromDict['value'] and fromDict['value']['profiles'] is not None:
29-
for profile in fromDict['value']['profiles']:
30-
LOG.warning('Charging profiles are not yet implemented %s', profile)
31+
for profileDict in fromDict['value']['profiles']:
32+
if 'id' in profileDict:
33+
if profileDict['id'] in self.profiles:
34+
self.profiles[profileDict['id']].update(fromDict=profileDict)
35+
else:
36+
self.profiles[profileDict['id']] = ChargingProfiles.ChargingProfile(fromDict=profileDict, parent=self.profiles)
37+
for profileId in [profileId for profileId in self.profiles.keys()
38+
if profileId not in [profile['id'] for profile in fromDict['value']['profiles'] if 'id' in profile]]:
39+
del self.profiles[profileId]
3140
else:
3241
self.profiles.clear()
3342
self.profiles.enabled = False
@@ -45,7 +54,170 @@ def __str__(self):
4554
if self.timeInCar.enabled:
4655
string += f'\n\tTime in Car: {self.timeInCar.value.isoformat()}' # pylint: disable=no-member
4756
string += f' (captured at {self.carCapturedTimestamp.value.isoformat()})' # pylint: disable=no-member
48-
string += f'\n\tProfiles: {len(self.profiles)} items'
49-
for profile in self.profiles:
50-
string += f'\n\t\t{profile}'
57+
string += f'\n\t\tProfiles: {len(self.profiles)} items'
58+
for profile in self.profiles.values():
59+
string += '\n' + ''.join(['\t\t\t' + line for line in str(profile).splitlines(True)])
5160
return string
61+
62+
class ChargingProfile(AddressableObject):
63+
def __init__(
64+
self,
65+
parent,
66+
fromDict=None
67+
):
68+
self.id = AddressableAttribute(localAddress='id', parent=self, value=None, valueType=int)
69+
self.name = AddressableAttribute(localAddress='name', parent=self, value=None, valueType=str)
70+
self.maxChargingCurrent = AddressableAttribute(localAddress='maxChargingCurrent', parent=self, value=None, valueType=str)
71+
self.minSOC_pct = AddressableAttribute(localAddress='minSOC_pct', parent=self, value=None, valueType=int)
72+
self.targetSOC_pct = AddressableAttribute(localAddress='targetSOC_pct', parent=self, value=None, valueType=int)
73+
self.timers = AddressableDict(localAddress='timers', parent=self)
74+
self.preferredChargingTimes = AddressableDict(localAddress='preferredChargingTimes', parent=self)
75+
self.options = None
76+
super().__init__(localAddress=None, parent=parent)
77+
78+
if fromDict is not None:
79+
self.update(fromDict)
80+
81+
def update(self, fromDict): # noqa: C901
82+
LOG.debug('Update charging profile from dict')
83+
84+
if 'id' in fromDict:
85+
self.id.fromDict(fromDict, 'id')
86+
self.localAddress = str(self.id.value)
87+
else:
88+
LOG.error('Charging Proile is missing id attribute')
89+
90+
self.name.fromDict(fromDict, 'name')
91+
self.maxChargingCurrent.fromDict(fromDict, 'maxChargingCurrent')
92+
self.minSOC_pct.fromDict(fromDict, 'minSOC_pct')
93+
self.targetSOC_pct.fromDict(fromDict, 'targetSOC_pct')
94+
95+
if 'options' in fromDict and fromDict['options'] is not None:
96+
if self.options is not None and self.options.enabled:
97+
self.options.update(fromDict=fromDict['options'])
98+
else:
99+
self.options = ChargingProfiles.ChargingProfile.Options(fromDict=fromDict['options'], parent=self)
100+
101+
if 'timers' in fromDict and fromDict['timers'] is not None:
102+
for chargingProfileTimerDict in fromDict['timers']:
103+
if 'id' in chargingProfileTimerDict:
104+
if chargingProfileTimerDict['id'] in self.timers:
105+
self.timers[chargingProfileTimerDict['id']].update(fromDict=chargingProfileTimerDict)
106+
else:
107+
self.timers[chargingProfileTimerDict['id']] = Timer(
108+
fromDict=chargingProfileTimerDict, parent=self.timers)
109+
for timerId in [timerId for timerId in self.timers.keys()
110+
if timerId not in [timer['id']
111+
for timer in fromDict['timers'] if 'id' in timer]]:
112+
del self.timers[timerId]
113+
else:
114+
self.timers.clear()
115+
self.timers.enabled = False
116+
117+
if 'preferredChargingTimes' in fromDict and fromDict['preferredChargingTimes'] is not None:
118+
for preferredChargingTimesDict in fromDict['preferredChargingTimes']:
119+
if 'id' in preferredChargingTimesDict:
120+
if preferredChargingTimesDict['id'] in self.preferredChargingTimes:
121+
self.preferredChargingTimes[preferredChargingTimesDict['id']].update(fromDict=preferredChargingTimesDict)
122+
else:
123+
self.preferredChargingTimes[preferredChargingTimesDict['id']] = ChargingProfiles.ChargingProfile.PreferredTime(
124+
fromDict=preferredChargingTimesDict, parent=self.preferredChargingTimes)
125+
for timeId in [timeId for timeId in self.preferredChargingTimes.keys()
126+
if timeId not in [timer['id']
127+
for timer in fromDict['timers'] if 'id' in timer]]:
128+
del self.preferredChargingTimes[timeId]
129+
else:
130+
self.preferredChargingTimes.clear()
131+
self.preferredChargingTimes.enabled = False
132+
133+
for key, value in {key: value for key, value in fromDict.items() if key not in ['id', 'name', 'maxChargingCurrent', 'minSOC_pct', 'targetSOC_pct',
134+
'timers', 'preferredChargingTimes', 'options']}.items():
135+
LOG.warning('%s: Unknown attribute %s with value %s', self.getGlobalAddress(), key, value)
136+
137+
def __str__(self):
138+
string = ''
139+
if self.id.enabled:
140+
string += f'Profile: {self.id.value}'
141+
if self.name.enabled:
142+
string += f' - {self.name.value}'
143+
if self.options is not None and self.options.enabled:
144+
string += f'\nOptions: {self.options}'
145+
if self.timers.enabled:
146+
string += f'\nTimers: {len(self.timers)} items'
147+
for timer in self.timers.values():
148+
string += ''.join(['\n\t' + line for line in str(timer).splitlines(True)])
149+
if self.preferredChargingTimes.enabled:
150+
string += f'\nPreferred Times: {len(self.preferredChargingTimes)} items'
151+
for preferredTime in self.preferredChargingTimes.values():
152+
string += ''.join(['\n\t' + line for line in str(preferredTime).splitlines(True)])
153+
return string
154+
155+
class PreferredTime(AddressableObject):
156+
def __init__(
157+
self,
158+
parent,
159+
fromDict=None,
160+
):
161+
super().__init__(localAddress=None, parent=parent)
162+
self.id = AddressableAttribute(localAddress='id', parent=self, value=None, valueType=int)
163+
self.preferredTimeEnabled = AddressableAttribute(localAddress='enabled', parent=self, value=None, valueType=bool)
164+
self.startTime = AddressableAttribute(localAddress='startTime', parent=self, value=None, valueType=time)
165+
self.endTime = AddressableAttribute(localAddress='endTime', parent=self, value=None, valueType=time)
166+
if fromDict is not None:
167+
self.update(fromDict)
168+
169+
def update(self, fromDict):
170+
LOG.debug('Update preferred time from dict')
171+
172+
if 'id' in fromDict:
173+
self.id.fromDict(fromDict, 'id')
174+
self.localAddress = str(self.id.value)
175+
else:
176+
LOG.error('Preferred time is missing id attribute')
177+
178+
self.preferredTimeEnabled.fromDict(fromDict, 'enabled')
179+
self.startTime.fromDict(fromDict, 'startTime')
180+
self.endTime.fromDict(fromDict, 'endTime')
181+
182+
for key, value in {key: value for key, value in fromDict.items()
183+
if key not in ['id', 'enabled', 'startTime', 'endTime']}.items():
184+
LOG.warning('%s: Unknown attribute %s with value %s', self.getGlobalAddress(), key, value)
185+
186+
def __str__(self):
187+
string = ''
188+
if self.id.enabled:
189+
string += f'{self.id.value}:'
190+
if self.preferredTimeEnabled.enabled:
191+
string += f' Enabled: {self.preferredTimeEnabled.value}'
192+
if self.startTime.enabled:
193+
string += f' Start: {self.startTime.value}'
194+
if self.endTime.enabled:
195+
string += f' End: {self.endTime.value}'
196+
return string
197+
198+
class Options(AddressableObject):
199+
def __init__(
200+
self,
201+
parent,
202+
fromDict=None,
203+
):
204+
super().__init__(localAddress='options', parent=parent)
205+
self.id = AddressableAttribute(localAddress='id', parent=self, value=None, valueType=int)
206+
self.autoUnlockPlugWhenCharged = AddressableAttribute(localAddress='autoUnlockPlugWhenCharged', value=None, parent=self,
207+
valueType=UnlockPlugState)
208+
if fromDict is not None:
209+
self.update(fromDict)
210+
211+
def update(self, fromDict):
212+
LOG.debug('Update preferred time from dict')
213+
214+
self.autoUnlockPlugWhenCharged.fromDict(fromDict, 'autoUnlockPlugWhenCharged')
215+
216+
for key, value in {key: value for key, value in fromDict.items() if key not in ['autoUnlockPlugWhenCharged']}.items():
217+
LOG.warning('%s: Unknown attribute %s with value %s', self.getGlobalAddress(), key, value)
218+
219+
def __str__(self):
220+
string = ''
221+
if self.autoUnlockPlugWhenCharged.enabled:
222+
string += f'\n\tAuto Unlock When Charged: {self.autoUnlockPlugWhenCharged.value.value}'
223+
return string

weconnect/elements/charging_settings.py

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from weconnect.addressable import AddressableLeaf, ChangeableAttribute
44
from weconnect.elements.generic_settings import GenericSettings
5-
from weconnect.elements.control_operation import ControlInputEnum
5+
from weconnect.elements.enums import UnlockPlugState, MaximumChargeCurrent
66

77
LOG = logging.getLogger("weconnect")
88

@@ -17,9 +17,9 @@ def __init__(
1717
fixAPI=True,
1818
):
1919
self.maxChargeCurrentAC = ChangeableAttribute(
20-
localAddress='maxChargeCurrentAC', parent=self, value=None, valueType=ChargingSettings.MaximumChargeCurrent)
20+
localAddress='maxChargeCurrentAC', parent=self, value=None, valueType=MaximumChargeCurrent)
2121
self.autoUnlockPlugWhenCharged = ChangeableAttribute(localAddress='autoUnlockPlugWhenCharged', value=None,
22-
parent=self, valueType=ChargingSettings.UnlockPlugState)
22+
parent=self, valueType=UnlockPlugState)
2323
self.targetSOC_pct = ChangeableAttribute(localAddress='targetSOC_pct', value=None, parent=self, valueType=int)
2424
super().__init__(vehicle=vehicle, parent=parent, statusId=statusId, fromDict=fromDict, fixAPI=fixAPI)
2525

@@ -59,23 +59,3 @@ def __str__(self):
5959
if self.targetSOC_pct.enabled:
6060
string += f'\n\tTarget SoC: {self.targetSOC_pct.value} %'
6161
return string
62-
63-
class UnlockPlugState(ControlInputEnum,):
64-
OFF = 'off'
65-
ON = 'on'
66-
PERMANENT = 'permanent'
67-
UNKNOWN = 'unknown'
68-
69-
@classmethod
70-
def allowedValues(cls):
71-
return [ChargingSettings.UnlockPlugState.OFF, ChargingSettings.UnlockPlugState.ON]
72-
73-
class MaximumChargeCurrent(ControlInputEnum,):
74-
MAXIMUM = 'maximum'
75-
REDUCED = 'reduced'
76-
INVALID = 'invalid'
77-
UNKNOWN = 'unknown'
78-
79-
@classmethod
80-
def allowedValues(cls):
81-
return [ChargingSettings.MaximumChargeCurrent.MAXIMUM, ChargingSettings.MaximumChargeCurrent.REDUCED]

0 commit comments

Comments
 (0)