|
| 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