Skip to content

Commit 3b6e479

Browse files
committed
Added csv_to_google_sheet.py
1 parent f16acbb commit 3b6e479

5 files changed

Lines changed: 268 additions & 7 deletions

File tree

README.md

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
- [smart\_plug\_mini.cfg](#smart_plug_minicfg)
88
- [smart\_plug\_mini.py](#smart_plug_minipy)
99
- [Example standard output of smart\_plug\_mini.py](#example-standard-output-of-smart_plug_minipy)
10+
- [csv\_to\_google\_sheet.py](#csv_to_google_sheetpy)
11+
- [Configuration of gspread for "python csv\_to\_google\_sheet.py"](#configuration-of-gspread-for-python-csv_to_google_sheetpy)
1012
- [Example crontab to run hourly on Raspberry Pi or another linux system](#example-crontab-to-run-hourly-on-raspberry-pi-or-another-linux-system)
1113
- [Sniffing the e-Control App](#sniffing-the-e-control-app)
1214
- [Month request/response](#month-requestresponse)
@@ -17,13 +19,15 @@
1719
- [24 hour response](#24-hour-response)
1820
- [Playing around with the server API](#playing-around-with-the-server-api)
1921

22+
---
2023
# Summary
2124

2225
Get history kWh consumption of broadlink Smart Plug Mini (tested with model SP3S-EU) using the broadlink server and append later kWh measurements to a .csv file format per device.
2326

2427
I have 2 times model Smart plug SP3S-EU and tested with those.
2528
Probably this also works for other broadlink SP mini models.
2629

30+
---
2731
# Quick start
2832

2933
- Make sure you have installed Python 3.9 or higher. [Here is more information about installing Python](https://realpython.com/installing-python/)
@@ -32,6 +36,7 @@ Probably this also works for other broadlink SP mini models.
3236
- Configure [smart_plug_mini.cfg](#smart_plug_minicfg), e.g. MAC addresses
3337
- Run the [smart_plug_mini.py](#smart_plug_minipy) to generate per configured device a DEVICE_NAME.csv file from the configured datetime onwards
3438

39+
---
3540
# Background
3641

3742
I am using 2 Smart plug SP3S-EU since 2018. The e-control App was limited in functionality/views. One of the strange things is e.g. the fact that you cannot see the month December of the year before the current year. So I was looking around if someone already had a better solution. I found the library [python-broadlink](https://github.com/mjg59/python-broadlink), but this was only for local access to your broadlink smart plug devices (get direct power measurements). So I decided to try to [sniff the e-Control app, see here](#sniffing-the-e-control-app).
@@ -41,6 +46,7 @@ In short, it appeared way easier to get the historical data of my smart plug min
4146
So I started making [a simple standalone python script smart_plug_mini.py which appends the history data per hour in a csv file for each device](#smart_plug_minipy) and made [some parts configurable](#smart_plug_minicfg).
4247
Also the tool writes the Day, Weeks, Months and Years summaries to separate .csv files per device.
4348

49+
---
4450
# python_broadlink_smart_plug_mini_info.py
4551

4652
All the credits goes to [python-broadlink library](https://github.com/mjg59/python-broadlink).
@@ -59,6 +65,7 @@ Remarks for this example output:
5965
- the MAC addresses are not my real SP3S-EU MAC addresses (I changed them, also in the rest of the examples)
6066
- you have to configure the MAC addresses 32:AA:31:72:63:43 and 32:AA:31:72:62:40 in [smart_plug_mini.cfg](#smart_plug_minicfg)
6167

68+
---
6269
# smart_plug_mini.cfg
6370

6471
[This configuration file](https://raw.githubusercontent.com/ZuinigeRijder/python-broadlink-smart-plug-mini/main/smart_plug_mini.cfg) needs to be configured once for the smart_plug_mini.py script.
@@ -83,6 +90,7 @@ Remarks to the configuration:
8390
- start_dates: comma separated list of date per device you want to start filling the history in .csv files (note: when .csv file already exists, the start_date is ignored and last entry of csv file is taken for later measurements)
8491
- time_filter: this is the timefilter for hourly measurements, maybe you want to play with this setting
8592

93+
---
8694
# smart_plug_mini.py
8795

8896
Simple Python3 script retrieve (history) kWh values for the configured Smart Plug mini devices.
@@ -104,7 +112,7 @@ OUTPUTFILES (for each configured DEVICE_NAME):
104112
- DEVICE_NAME.months.csv: months summary, [example](https://raw.githubusercontent.com/ZuinigeRijder/python-broadlink-smart-plug-mini/main/examples/Badkamer.months.csv)
105113
- DEVICE_NAME.years.csv: years summary, [example](https://raw.githubusercontent.com/ZuinigeRijder/python-broadlink-smart-plug-mini/main/examples/Badkamer.years.csv)
106114

107-
115+
---
108116
## Example standard output of smart_plug_mini.py
109117

110118
```
@@ -177,6 +185,77 @@ Some remarks from this example:
177185
A screenshot for [example spreadsheet Badkamer.xlsx](https://raw.githubusercontent.com/ZuinigeRijder/python-broadlink-smart-plug-mini/main/examples/Badkamer.xlsx) which has imported a larger Badkamer.csv:
178186
- ![alt text](https://raw.githubusercontent.com/ZuinigeRijder/python-broadlink-smart-plug-mini/main/examples/Badkamer.xlsx.jpg)
179187

188+
---
189+
# csv_to_google_sheet.py
190+
191+
Simple Python3 script to read the smart_plug_mini.py generated csv files and write a summary for each device to a separate Google spreadsheet.
192+
193+
Note: you need to install the package gspread and configure gspread, [see here for the configuration](#configuration-of-gspread-for-python-csv_to_google_sheetpy)
194+
195+
Usage:
196+
```
197+
python csv_to_google_sheet.py
198+
```
199+
INPUTFILES:
200+
- smart_plug_mini.cfg
201+
- for each configured DEVICE_NAME:
202+
- - DEVICE_NAME.csv
203+
- - DEVICE_NAME.days.csv
204+
- - DEVICE_NAME.weeks.csv
205+
- - DEVICE_NAME.months.csv
206+
- - DEVICE_NAME.years.csv
207+
208+
Standard output:
209+
- progress of what is done
210+
211+
OUTPUT SPREADSHEET:
212+
- DEVICE_NAME.SP (for each configured DEVICE_NAME)
213+
214+
So the smart_plug_mini.py tool runs on my Raspberry Pi server, but I want to look at the results, without having to login to my server. So another tool has been made, which copies a summary to a google spreadsheet for each device: csv_to_google_sheet.py
215+
216+
The Google spreadsheet contains kWh usage, including nice diagrams, when you [import the example Badkamer.SP.xlsx spreadsheet in Google Spreadsheet](https://raw.githubusercontent.com/ZuinigeRijder/python-broadlink-smart-plug-mini/main/examples/Badkamer.SP.xlsx):
217+
- last written Date, Time, kWh, Hour, Day, Week, Month, Year
218+
- last 48 hours
219+
- last 32 days
220+
- last 27 weeks
221+
- last 25 months
222+
- last 25 years ;-)
223+
224+
A short video of how it can look on an Android phone, [can be found here](https://www.youtube.com/shorts/p4IWoX7yNpE).
225+
Of course you can also view the Google Spreadsheet on your computer or tablet, e.g. Windows or Mac.
226+
227+
<a href="http://www.youtube.com/watch?feature=player_embedded&v=p4IWoX7yNpE" target="_blank"><img src="http://img.youtube.com/vi/p4IWoX7yNpE/0.jpg" alt="Broadlink Smart Plug Mini showing csv results in Google Spreadsheet" width="240" height="180" border="10" /></a>
228+
229+
---
230+
# Configuration of gspread for "python csv_to_google_sheet.py"
231+
For updating a Google Spreadsheet, csv_to_google_sheet.py is using the package gspread.
232+
For Authentication with Google Spreadsheet you have to configure authentication for gspread.
233+
This [authentication configuration is described here](https://docs.gspread.org/en/latest/oauth2.html)
234+
235+
The csv_to_google_sheet.py script uses access to the Google spreadsheets on behalf of a bot account using Service Account.
236+
237+
Follow the steps in this link above, here is the summary of these steps:
238+
- Enable API Access for a Project
239+
- - Head to [Google Developers Console](https://console.developers.google.com/) and create a new project (or select the one you already have).
240+
- - In the box labeled "Search for APIs and Services", search for "Google Drive API" and enable it.
241+
- - In the box labeled "Search for APIs and Services", search for "Google Sheets API" and enable it
242+
- For Bots: Using Service Account
243+
- - Go to "APIs & Services > Credentials" and choose "Create credentials > Service account key".
244+
- - Fill out the form
245+
- - Click "Create" and "Done".
246+
- - Press "Manage service accounts" above Service Accounts.
247+
- - Press on : near recently created service account and select "Manage keys" and then click on "ADD KEY > Create new key".
248+
- - Select JSON key type and press "Create".
249+
- - You will automatically download a JSON file with credentials
250+
- - Remember the path to the downloaded credentials json file. Also, in the next step you will need the value of client_email from this file.
251+
- - Move the downloaded json file to ~/.config/gspread/service_account.json. Windows users should put this file to %APPDATA%\gspread\service_account.json.
252+
- Setup a Google Spreasheet to be updated by csv_to_google_sheet.py (for each device one google spreadsheet)
253+
- - In Google Spreadsheet, create an empty Google Spreadsheet with the name: DEVICE_NAME.SP (or [import the example Badkamer.SP.xlsx spreadsheet in Google Spreadsheet and rename it to DEVICE_NAME.SP](https://raw.githubusercontent.com/ZuinigeRijder/python-broadlink-smart-plug-mini/main/examples/Badkamer.SP.xlsx))
254+
- - Go to your spreadsheet and share it with the client_email from the step above (inside service_account.json)
255+
- run "python csv_to_google_sheet.py" and if everything is correct, the DEVICE_NAME.SP will be updated with a summary of the .csv files
256+
- configure to run "python csv_to_google_sheet.py" regularly, after having run "python smart_plug_mini.py"
257+
258+
---
180259
# Example crontab to run hourly on Raspberry Pi or another linux system
181260

182261
Example script [run_smart_plug_mini_once.sh](https://raw.githubusercontent.com/ZuinigeRijder/python-broadlink-smart-plug-mini/main/examples/run_smart_plug_mini_once.sh) to run smart_plug_mini.py on a linux based system.
@@ -186,13 +265,14 @@ Steps:
186265
- copy run_smart_plug_mini_once.sh, smart_plug_mini.cfg and smart_plug_mini.py in this smart_plug_mini directory
187266
- change inside smart_plug_mini.cfg the smart_plug_mini settings
188267
- chmod + x run_smart_plug_mini_once.sh
268+
- optionally: add running "python csv_to_google_sheet.py" to this script
189269

190270
Add the following line in your crontab -e to run it once per hour 9 minutes later (crontab -e):
191271
```
192272
9 * * * * ~/smart_plug_mini/run_smart_plug_mini_once.sh >> ~/smart_plug_mini/run_smart_plug_mini_once.log 2>&1
193273
```
194274

195-
275+
---
196276
# Sniffing the e-Control App
197277

198278
For the ones who also want to be able to sniff the calls from e-Control the App, this is how I did it (do it at your own risk):

csv_to_google_sheet.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
# == csv_to_google_sheet.py Author: Zuinige Rijder =====================================
2+
"""
3+
Simple Python3 script to update google sheet with
4+
csv input files generated by smart_plug_mini.py
5+
"""
6+
import configparser
7+
import os
8+
import traceback
9+
import time
10+
from datetime import datetime
11+
from pathlib import Path
12+
from typing import Generator
13+
import gspread
14+
15+
16+
def log(msg: str) -> None:
17+
"""log a message prefixed with a date/time format yyyymmdd hh:mm:ss"""
18+
print(datetime.now().strftime("%Y%m%d %H:%M:%S") + ": " + msg)
19+
20+
21+
def read_reverse_order(file_name: str) -> Generator[str, None, None]:
22+
"""read in reverse order"""
23+
# Open file for reading in binary mode
24+
with open(file_name, "rb") as read_obj:
25+
# Move the cursor to the end of the file
26+
read_obj.seek(0, os.SEEK_END)
27+
# Get the current position of pointer i.e eof
28+
pointer_location = read_obj.tell()
29+
# Create a buffer to keep the last read line
30+
buffer = bytearray()
31+
# Loop till pointer reaches the top of the file
32+
while pointer_location >= 0:
33+
# Move the file pointer to the location pointed by pointer_location
34+
read_obj.seek(pointer_location)
35+
# Shift pointer location by -1
36+
pointer_location = pointer_location - 1
37+
# read that byte / character
38+
new_byte = read_obj.read(1)
39+
# If the read byte is newline character then one line is read
40+
if new_byte == b"\n":
41+
# Fetch the line from buffer and yield it
42+
yield buffer.decode()[::-1]
43+
# Reinitialize the byte array to save next line
44+
buffer = bytearray()
45+
else:
46+
# If last read character is not eol then add it in buffer
47+
buffer.extend(new_byte)
48+
# If there is still data in buffer, then it is first line.
49+
if len(buffer) > 0:
50+
# Yield the first line too
51+
yield buffer.decode()[::-1]
52+
53+
54+
def get_last_line(filename: Path) -> str:
55+
"""get last line of filename"""
56+
last_line = ""
57+
if filename.is_file():
58+
with open(filename.name, "rb") as file:
59+
try:
60+
file.seek(-2, os.SEEK_END)
61+
while file.read(1) != b"\n":
62+
file.seek(-2, os.SEEK_CUR)
63+
except OSError:
64+
file.seek(0)
65+
last_line = file.readline().decode().strip()
66+
print(f"# last line {filename.name}: {last_line}")
67+
return last_line
68+
69+
70+
def read_csv_and_write_to_sheet(
71+
array: list, name: str, period: str, params: tuple[str, int, int, int, int, int]
72+
) -> list:
73+
"""read_csv_and_write_to_sheet"""
74+
last_line = params[0].split(",")
75+
last_line_index = params[1]
76+
startrow = params[2]
77+
endrow = startrow + params[3]
78+
strip_begin = params[4]
79+
strip_end = params[5]
80+
81+
row = startrow
82+
array.append({"range": f"A{row}:B{row}", "values": [[period, "kWh"]]})
83+
row += 1
84+
if len(last_line) == 7:
85+
date_str = last_line[0].strip()[strip_begin:strip_end]
86+
kwh = float(last_line[last_line_index].strip())
87+
array.append({"range": f"A{row}:B{row}", "values": [[date_str, kwh]]})
88+
row += 1
89+
if period == "Hour":
90+
csv_postfix = ""
91+
else:
92+
csv_postfix = f".{period.lower()}s"
93+
csv_filename = Path(f"{name}{csv_postfix}.csv")
94+
if csv_filename.is_file():
95+
log(f"### read_reverse_csv_file: {csv_filename.name} " + "#" * 30)
96+
for line in read_reverse_order(csv_filename.name):
97+
line = line.strip()
98+
if line == "":
99+
continue
100+
print(line)
101+
splitted = line.split(",")
102+
if len(splitted) > 2 and splitted[0].startswith("20"):
103+
date_str = splitted[0].strip()[strip_begin:strip_end]
104+
kwh = float(splitted[2].strip())
105+
array.append({"range": f"A{row}:B{row}", "values": [[date_str, kwh]]})
106+
row += 1
107+
108+
if row > endrow:
109+
break # finished
110+
else:
111+
log(f"Warning: csv file {csv_filename.name} does not exist")
112+
113+
return array
114+
115+
116+
def write_to_sheet(name: str, sheet: list) -> None:
117+
"""write_to_sheet"""
118+
array = []
119+
row = 1
120+
last = get_last_line(Path(f"{name}.csv"))
121+
last_split = last.split(",")
122+
if len(last_split) != 7 or not last_split[0].strip().startswith("20"):
123+
log(f"ERROR: unexpected last line in {name}.csv: {last}")
124+
return
125+
header = [["Date", "Time", "kWh", "Hour", "Day", "Week", "Month", "Year"]]
126+
array.append({"range": f"A{row}:H{row}", "values": header})
127+
row += 1
128+
first_line = [
129+
[
130+
last_split[0].strip().split(" ")[0].strip()[2:],
131+
last_split[0].strip().split(" ")[1].strip(),
132+
float(last_split[1].strip()),
133+
float(last_split[2].strip()),
134+
float(last_split[3].strip()),
135+
float(last_split[4].strip()),
136+
float(last_split[5].strip()),
137+
float(last_split[6].strip()),
138+
]
139+
]
140+
array.append({"range": f"A{row}:H{row}", "values": first_line})
141+
142+
# params(last_line, last_line_index, start, rows, strip_start, strip_end)
143+
array = read_csv_and_write_to_sheet(array, name, "Hour", ("", 2, 3, 49, 8, 16))
144+
array = read_csv_and_write_to_sheet(array, name, "Day", (last, 3, 54, 33, 5, 10))
145+
array = read_csv_and_write_to_sheet(array, name, "Week", (last, 4, 89, 28, 5, 10))
146+
array = read_csv_and_write_to_sheet(array, name, "Month", (last, 5, 119, 26, 0, 7))
147+
array = read_csv_and_write_to_sheet(array, name, "Year", (last, 6, 147, 25, 0, 4))
148+
149+
sheet.batch_update(array)
150+
151+
152+
def main() -> None:
153+
"""main"""
154+
parser = configparser.ConfigParser()
155+
parser.read("smart_plug_mini.cfg")
156+
sp3s_settings = dict(parser.items("smart_plug_mini"))
157+
158+
client = gspread.service_account()
159+
for name in sp3s_settings["device_names"].split(","):
160+
name = name.strip()
161+
retries = 2
162+
while retries > 0:
163+
try:
164+
spreadsheet_name = f"{name}.SP"
165+
log(f"##### Writing {spreadsheet_name} " + "#" * 60)
166+
spreadsheet = client.open(spreadsheet_name)
167+
sheet = spreadsheet.sheet1
168+
sheet.clear()
169+
write_to_sheet(name, sheet)
170+
171+
retries = 0
172+
except Exception as ex: # pylint: disable=broad-except
173+
log("Exception: " + str(ex))
174+
traceback.print_exc()
175+
retries -= 1
176+
log("Sleeping a minute")
177+
time.sleep(60)
178+
179+
180+
main()

examples/Badkamer.SP.xlsx

30.6 KB
Binary file not shown.

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
python_version>=3.9
22
broadlink==0.18.3
33
python_dateutil==2.8.2
4+
gspread==5.6.2

smart_plug_mini.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ def get_last_info_from_csv(
259259
last_m = 0.0
260260
last_y = 0.0
261261

262-
print(f"##### {csv_filename} ######################################")
262+
log(f"##### {csv_filename} ######################################")
263263
last_line = get_last_line(csv_filename)
264264
if last_line != "":
265265
splitted = last_line.split(",")
@@ -277,7 +277,7 @@ def get_last_info_from_csv(
277277
if last_date_server > date_start_server:
278278
last_date_server += relativedelta(hours=1) # start with next hour
279279
date_start_server = last_date_server
280-
print(f" {last_date_str}")
280+
log(f" {last_date_str}")
281281

282282
return date_start_server, last_kwh, last_d, last_w, last_m, last_y
283283

@@ -331,12 +331,12 @@ def do_kwh_counters() -> None:
331331
delta_m,
332332
delta_y,
333333
) = get_last_info_from_csv(Path(f"{DEVICE_NAME}.csv"))
334-
print(f"date_start local: {date_start_server.astimezone(ZONE_INFO_LOCAL)}")
335-
print(f"now local : {now_server.astimezone(ZONE_INFO_LOCAL)}")
334+
log(f"date_start local: {date_start_server.astimezone(ZONE_INFO_LOCAL)}")
335+
log(f"now local : {now_server.astimezone(ZONE_INFO_LOCAL)}")
336336
while date_start_server < now_server:
337337
prev_date = date_start_server.astimezone(ZONE_INFO_LOCAL)
338338
date_end_server = date_start_server + relativedelta(months=1)
339-
print(
339+
log(
340340
f"{DEVICE_NAME}: from {date_start_server.astimezone(ZONE_INFO_LOCAL)} to {date_end_server.astimezone(ZONE_INFO_LOCAL)}" # noqa
341341
)
342342
result = get_kwh_counters(

0 commit comments

Comments
 (0)