Skip to content

Commit 197c653

Browse files
authored
Merge pull request #195 from DJDevon3/main
Add Rachio Irrigation API Example
2 parents 420c242 + 0d9d6dc commit 197c653

File tree

1 file changed

+224
-0
lines changed

1 file changed

+224
-0
lines changed
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
# SPDX-FileCopyrightText: 2024 DJDevon3
2+
# SPDX-License-Identifier: MIT
3+
# Coded for Circuit Python 9.x
4+
"""Rachio Irrigation Timer API Example"""
5+
6+
import os
7+
import time
8+
9+
import adafruit_connection_manager
10+
import wifi
11+
12+
import adafruit_requests
13+
14+
# Rachio API Key required (comes with purchase of a device)
15+
# API is rate limited to 1700 calls per day.
16+
# https://support.rachio.com/en_us/public-api-documentation-S1UydL1Fv
17+
# https://rachio.readme.io/reference/getting-started
18+
RACHIO_KEY = os.getenv("RACHIO_APIKEY")
19+
20+
# Get WiFi details, ensure these are setup in settings.toml
21+
ssid = os.getenv("CIRCUITPY_WIFI_SSID")
22+
password = os.getenv("CIRCUITPY_WIFI_PASSWORD")
23+
24+
# API Polling Rate
25+
# 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour
26+
SLEEP_TIME = 900
27+
28+
# Set debug to True for full JSON response.
29+
# WARNING: absolutely shows extremely sensitive personal information & credentials
30+
# Including your real name, latitude, longitude, account id, mac address, etc...
31+
DEBUG = False
32+
33+
# Initalize Wifi, Socket Pool, Request Session
34+
pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio)
35+
ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio)
36+
requests = adafruit_requests.Session(pool, ssl_context)
37+
38+
RACHIO_HEADER = {"Authorization": " Bearer " + RACHIO_KEY}
39+
RACHIO_SOURCE = "https://api.rach.io/1/public/person/info/"
40+
RACHIO_PERSON_SOURCE = "https://api.rach.io/1/public/person/"
41+
42+
43+
def obfuscating_asterix(obfuscate_object, direction, characters=2):
44+
"""
45+
Obfuscates a string with asterisks except for a specified number of characters.
46+
param object: str The string to obfuscate with asterisks
47+
param direction: str Option either 'prepend', 'append', or 'all' direction
48+
param characters: int The number of characters to keep unobfuscated (default is 2)
49+
"""
50+
object_len = len(obfuscate_object)
51+
if direction not in {"prepend", "append", "all"}:
52+
raise ValueError("Invalid direction. Use 'prepend', 'append', or 'all'.")
53+
if characters >= object_len and direction != "all":
54+
# If characters greater than or equal to string length,
55+
# return the original string as it can't be obfuscated.
56+
return obfuscate_object
57+
asterix_replace = "*" * object_len
58+
if direction == "append":
59+
asterix_final = obfuscate_object[:characters] + "*" * (object_len - characters)
60+
elif direction == "prepend":
61+
asterix_final = "*" * (object_len - characters) + obfuscate_object[-characters:]
62+
elif direction == "all":
63+
# Replace all characters with asterisks
64+
asterix_final = asterix_replace
65+
66+
return asterix_final
67+
68+
69+
def time_calc(input_time):
70+
"""Converts seconds to minutes/hours/days"""
71+
if input_time < 60:
72+
return f"{input_time:.0f} seconds"
73+
if input_time < 3600:
74+
return f"{input_time / 60:.0f} minutes"
75+
if input_time < 86400:
76+
return f"{input_time / 60 / 60:.0f} hours"
77+
return f"{input_time / 60 / 60 / 24:.1f} days"
78+
79+
80+
def _format_datetime(datetime):
81+
"""F-String formatted struct time conversion"""
82+
return (
83+
f"{datetime.tm_mon:02}/"
84+
+ f"{datetime.tm_mday:02}/"
85+
+ f"{datetime.tm_year:02} "
86+
+ f"{datetime.tm_hour:02}:"
87+
+ f"{datetime.tm_min:02}:"
88+
+ f"{datetime.tm_sec:02}"
89+
)
90+
91+
92+
while True:
93+
# Connect to Wi-Fi
94+
print("\nConnecting to WiFi...")
95+
while not wifi.radio.ipv4_address:
96+
try:
97+
wifi.radio.connect(ssid, password)
98+
except ConnectionError as e:
99+
print("❌ Connection Error:", e)
100+
print("Retrying in 10 seconds")
101+
print("✅ Wifi!")
102+
103+
try:
104+
print(" | Attempting to GET Rachio Authorization")
105+
try:
106+
with requests.get(
107+
url=RACHIO_SOURCE, headers=RACHIO_HEADER
108+
) as rachio_response:
109+
rachio_json = rachio_response.json()
110+
except ConnectionError as e:
111+
print("Connection Error:", e)
112+
print("Retrying in 10 seconds")
113+
print(" | ✅ Authorized")
114+
115+
rachio_id = rachio_json["id"]
116+
if DEBUG:
117+
print(" | | Person ID: ", rachio_id)
118+
print(" | | This ID will be used for subsequent calls")
119+
print("\nFull API GET URL: ", RACHIO_SOURCE)
120+
print(rachio_json)
121+
122+
except (ValueError, RuntimeError) as e:
123+
print(f"Failed to get data, retrying\n {e}")
124+
time.sleep(60)
125+
break
126+
127+
try:
128+
print(" | Attempting to GET Rachio JSON")
129+
try:
130+
with requests.get(
131+
url=RACHIO_PERSON_SOURCE + rachio_id, headers=RACHIO_HEADER
132+
) as rachio_response:
133+
rachio_json = rachio_response.json()
134+
except ConnectionError as e:
135+
print("Connection Error:", e)
136+
print("Retrying in 10 seconds")
137+
print(" | ✅ Rachio JSON")
138+
139+
rachio_id = rachio_json["id"]
140+
rachio_id_ast = obfuscating_asterix(rachio_id, "append", 3)
141+
print(" | | UserID: ", rachio_id_ast)
142+
143+
rachio_username = rachio_json["username"]
144+
rachio_username_ast = obfuscating_asterix(rachio_username, "append", 3)
145+
print(" | | Username: ", rachio_username_ast)
146+
147+
rachio_name = rachio_json["fullName"]
148+
rachio_name_ast = obfuscating_asterix(rachio_name, "append", 3)
149+
print(" | | Full Name: ", rachio_name_ast)
150+
151+
rachio_deleted = rachio_json["deleted"]
152+
if not rachio_deleted:
153+
print(" | | Account Status: Active")
154+
else:
155+
print(" | | Account Status?: Deleted!")
156+
157+
rachio_createdate = rachio_json["createDate"]
158+
rachio_timezone_offset = rachio_json["devices"][0]["utcOffset"]
159+
# Rachio Unix time is in milliseconds, convert to seconds
160+
rachio_createdate_seconds = rachio_createdate // 1000
161+
rachio_timezone_offset_seconds = rachio_timezone_offset // 1000
162+
# Apply timezone offset in seconds
163+
local_unix_time = rachio_createdate_seconds + rachio_timezone_offset_seconds
164+
if DEBUG:
165+
print(f" | | Unix Registration Date: {rachio_createdate}")
166+
print(f" | | Unix Timezone Offset: {rachio_timezone_offset}")
167+
current_struct_time = time.localtime(local_unix_time)
168+
final_timestamp = "{}".format(_format_datetime(current_struct_time))
169+
print(f" | | Registration Date: {final_timestamp}")
170+
171+
rachio_devices = rachio_json["devices"][0]["name"]
172+
print(" | | Device: ", rachio_devices)
173+
174+
rachio_model = rachio_json["devices"][0]["model"]
175+
print(" | | | Model: ", rachio_model)
176+
177+
rachio_serial = rachio_json["devices"][0]["serialNumber"]
178+
rachio_serial_ast = obfuscating_asterix(rachio_serial, "append")
179+
print(" | | | Serial Number: ", rachio_serial_ast)
180+
181+
rachio_mac = rachio_json["devices"][0]["macAddress"]
182+
rachio_mac_ast = obfuscating_asterix(rachio_mac, "append")
183+
print(" | | | MAC Address: ", rachio_mac_ast)
184+
185+
rachio_status = rachio_json["devices"][0]["status"]
186+
print(" | | | Device Status: ", rachio_status)
187+
188+
rachio_timezone = rachio_json["devices"][0]["timeZone"]
189+
print(" | | | Time Zone: ", rachio_timezone)
190+
191+
# Latitude & Longtitude are used for smart watering & rain delays
192+
rachio_latitude = str(rachio_json["devices"][0]["latitude"])
193+
rachio_lat_ast = obfuscating_asterix(rachio_latitude, "all")
194+
print(" | | | Latitude: ", rachio_lat_ast)
195+
196+
rachio_longitude = str(rachio_json["devices"][0]["longitude"])
197+
rachio_long_ast = obfuscating_asterix(rachio_longitude, "all")
198+
print(" | | | Longitude: ", rachio_long_ast)
199+
200+
rachio_rainsensor = rachio_json["devices"][0]["rainSensorTripped"]
201+
print(" | | | Rain Sensor: ", rachio_rainsensor)
202+
203+
rachio_zone0 = rachio_json["devices"][0]["zones"][0]["name"]
204+
rachio_zone1 = rachio_json["devices"][0]["zones"][1]["name"]
205+
rachio_zone2 = rachio_json["devices"][0]["zones"][2]["name"]
206+
rachio_zone3 = rachio_json["devices"][0]["zones"][3]["name"]
207+
zones = f"{rachio_zone0}, {rachio_zone1}, {rachio_zone2}, {rachio_zone3}"
208+
print(f" | | | Zones: {zones}")
209+
210+
if DEBUG:
211+
print(f"\nFull API GET URL: {RACHIO_PERSON_SOURCE+rachio_id}")
212+
print(rachio_json)
213+
214+
print("\nFinished!")
215+
print(f"Board Uptime: {time_calc(time.monotonic())}")
216+
print(f"Next Update: {time_calc(SLEEP_TIME)}")
217+
print("===============================")
218+
219+
except (ValueError, RuntimeError) as e:
220+
print(f"Failed to get data, retrying\n {e}")
221+
time.sleep(60)
222+
break
223+
224+
time.sleep(SLEEP_TIME)

0 commit comments

Comments
 (0)