Repository navigation
Expand file tree
/
Copy pathiog-sensor.1.py
More file actions
executable file
·195 lines (150 loc) · 5.73 KB
/
Copy pathiog-sensor.1.py
File metadata and controls
executable file
·195 lines (150 loc) · 5.73 KB
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
#!/usr/bin/env python3
# A script to process IOG slots and record any bonus ones
# Times are represented in units of 30 minutes in local timezone (ie +1 hour in bst):
# 0 = 00:00
# 1 = 00:30
# 11 = 05:30
# 47 = 23:30
import logging
import time
import gzip
from datetime import datetime
from octopus import IOG
from sensors import Sensors
_logger = logging.getLogger(__name__)
def simplify(d):
"""Return a simplified dispatch, where times are reduced to multiples of 30 minutes"""
# dispatches are provided with timezone info (currently utc). Shift them to local timezone
s = datetime.fromisoformat(d["start"]).astimezone()
sh = s.hour
sm = s.minute
# Note that rounding up may result in time of 48 rather than 0.
# eg start of 23:55 will become half-hour slot number 48
# start time is usually rounded down, but not if mins are very close to end of half-hour period
start = sh * 2
if sm >= 55:
start = start + 2 # round up to next hour
elif sm >= 25:
start = start + 1 # round to half hour
# else round down to start of hour
e = datetime.fromisoformat(d["end"]).astimezone()
eh = e.hour
em = e.minute
# end is usually rounded up, unless it's just over the half-hour boundary
end = eh * 2
if em >= 35:
end = end + 2 # round to next hour
elif em >= 5:
end = end + 1
# else round down to whole hour
return (start, end)
def process(pending):
# first, sort using string form of times
pending.sort(key = lambda d : d["start"])
for p in pending:
_logger.debug(p)
# then convert to simplified times
pending = [simplify(d) for d in pending]
#print(pending)
# Now split each piece into up to 3:
# A) from 00:00 to 05:30 (0 to 11)
# B) from 05:30 to 23:30 (11 to 47)
# C) from 23:30 to 00:00 (47 to 0/48)
# and discard (A) and (C)
# Iterate backwards so that creating new pieces doesn't break the index
# Now go through and turn each segment into 0, 1 or 2 pieces
# Ignore the possibility that we charge for 18 hours, from cheap rate of
# one day to cheap rate of next day
for after in range(len(pending),0,-1):
(a, b) = pending[after - 1]
if a == b:
# probably very short slot got rounded to empty
result = ()
elif 11 <= a < 47:
if 11 < b <= 47:
if a < b:
# entirely during the day - nothing to change
continue
else:
# starts before 23:30 and ends after 05:30 so need to split
result = ((a, 47), (11, b))
else:
# a is daytime, b is nighttime
result = ( (a, 47), )
elif 11 < b <= 47:
# a is nighttime, b is daytime
result = ( (11, b), )
else:
# both are nighttime
result = ()
pending[after-1:after] = result
#print(pending)
# Finally, merge adjacent entries.
# Again, iterate backwards through the list - this way,
# even if we delete some entries, we don't lose
# our place
for idx in range(len(pending)-1,0,-1):
before = pending[idx-1]
after = pending[idx]
if (before[0] <= after[0] <= before[1]):
pending[idx-1:idx+1] = ((before[0], after[1]),)
return pending
def main():
"""Maintain the iog state in the Sensors memory-map."""
sensors = Sensors()
iog = IOG()
while True:
now = time.time()
# car spends most of its time unplugged, so we don't
# need to trouble GraphQL
zappi = sensors.load(Sensors.Zappi)
if zappi.status <= 0:
planned = []
s = Sensors.IOG(int(now), 0, 0)
else:
planned = iog.getDispatches()['plannedDispatches']
total = len(planned)
planned = process(planned)
# print(planned)
count = len(planned)
if count > 3:
count = 3
args=[int(now), total, count]
for (a, b) in planned:
args.append(a)
args.append(b)
s = Sensors.IOG(*args)
sensors.store(s)
_logger.info(s)
time.sleep(900)
def test():
# some test data
t = [{'start': '2024-11-16T22:30:00+00:00', 'end': '2024-11-17T01:00:00+00:00', 'delta': '-1.77'},
{'start': '2024-11-17T04:30:00+00:00', 'end': '2024-11-17T05:00:00+00:00', 'delta': '-1.77'},
{'start': '2024-11-17T05:00:00+00:00', 'end': '2024-11-17T06:30:00+00:00', 'delta': '-1.12'}]
out = process(t)
print(out)
t = [{'start': '2024-11-17T05:00:00+00:00', 'end': '2024-11-17T06:00:00+00:00', 'delta': '-1.77'},
{'start': '2024-11-17T06:30:00+00:00', 'end': '2024-11-17T06:45:00+00:00', 'delta': '-0.89'},
{'start': '2024-11-17T06:45:00+00:00', 'end': '2024-11-17T07:00:00+00:00', 'delta': '-0.23'},
{'start': '2024-11-17T07:00:00+00:00', 'end': '2024-11-17T07:30:00+00:00', 'delta': '-1.77'}]
out = process(t)
print(out)
def setup_logging(zf):
# log to both file and console
gz_log_handler = logging.StreamHandler(zf)
_logger.addHandler(gz_log_handler)
stderr_log_handler = logging.StreamHandler()
_logger.addHandler(stderr_log_handler)
# prefix timestamp onto the file logger
formatter = logging.Formatter(
fmt="%(asctime)s: %(message)s", datefmt="%Y-%m-%d--%H:%M"
)
gz_log_handler.setFormatter(formatter)
_logger.setLevel(logging.DEBUG)
if __name__ == "__main__":
now = datetime.now()
tstamp = now.strftime("%Y%m%d-%H%M")
with gzip.open(filename="/tmp/iog." + tstamp + ".log.gz", mode="wt") as zf:
setup_logging(zf)
main()