KoreLogic Blog
Unplugging An IoT Device From The Cloud 2015-12-11 17:45

Hello again and welcome back. This is part two in our four-part series on firmware and embedded devices. Today, I will be discussing home automation and the Internet of Things (IoT). More specifically, I'll be talking about Blossom. Blossom is a cloud-based smart lawn watering system that will 'automatically' water your lawn. Normally, our goal is to break into the target device so I may inspect running processes and resident binaries to ensure they are not designed to work in ways that are counter to our interests. Today, I won't be doing that. Instead, I am going to observe the functionality of the device and how it interacts with the manufacturers cloud-based API. Then, I'll force network traffic redirection from the device to a server I control. Finally, I will recreate a bare minimum copy of the manufacturer's API available internally so that the device will no longer require internet access for a somewhat normal operation.

What does this mean? I am going to write an application to water my lawn, when I want my lawn watered. Why? Because I like the functionality of smart-enabled devices, but I do not like adding network potential pivot points anywhere on my networks. My hope is that this part in our series serves as a soft introduction into the thought process I typically use when removing an unwanted third-party from my networks or even attempting to attack the underlying software of a target device.

So, how does the Blossom work? The first thing Blossom asks you to do is move your wiring over to the new system, but I won't cover that. Once you power it up, Blossom will start an Access Point that you can connect to. The access point will be named as such: Blossom-XXXX where 'X' is a four digit number. Once you install the corresponding phone app, it wants you to create an account and input some information about the geographic location where the system resides. In retrospect, since I don't care about managing my lawn settings while say ... traveling or while being down the street at a friends, creating an account and providing said information may be irrelevant. There is also an HTTP portal and API which does not require any of that information.

The wireless password for every Blossom unit is '12flowers'. Once connected to the Blossom access point, you should visit the default web portal (http://192.168.10.1). The latest Blossom firmware will disable the access point after setup, but it will also disable the web portal and API I use. My advice is to not update the firmware. However, a bug in the older firmware exists where the access point does not get torn down after setup. This was a concern to me as a separate wireless adapter on the Blossom also exists, which is connected to an internet routable network. A security issue in the Blossom web application or WSGI API could result in another easy pivot point. Fortunately, after emailing Blossom's support group they agreed to provide me with a firmware build that would disable the access point while retaining the web server and API. I wish other vendors would be as responsive to my requests. Bravo Blossom!

Here you can configure a few things within the Blossom. The four main sections within the UI are named: Provisioning, System Info, Advanced, and Blossom. Provisioning allows you to connect Blossom to your wireless access point. System Info displays basic network adapter and operating system information. Advanced allows the wireless access point settings to be changed, firmware to be upgraded, the device can be rebooted, and reset to the factory issued state. The Blossom section allows the user to change the 'Server Group' between Live, Staging, and Test. I haven't tested to see if changing this value alters system settings such as running services.

POST /sys/network HTTP/1.1
Host: [redacted]
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:42.0) Gecko/20100101 Firefox/42.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
DNT: 1
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Referer: http://[redacted]/
Content-Length: 175
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache

{"ssid":"korelogicwashere","security":4,"key":"sillyfuntimes","ip":0,"ipaddr":"192.168.2.2","ipmask":"255.255.255.0","ipgw":"192.168.2.1","ipdns1":"0.0.0.0","ipdns2":"0.0.0.0"}

They've tried to hide some non-critical things, but it doesn't seem like they put a lot of effort into it. For example, you can open and close valves artibrarily and also change the LED colors! Their method of hiding the functionality was to comment out the associated javascript which displays that part of the UI. They didn't actually remove the underlying functionality. So if you just view source:

Example requests for those looked like:

For the LED:

POST /bloom/led_custom HTTP/1.1
Host: [redacted]
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:42.0) Gecko/20100101 Firefox/42.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
DNT: 1
Content-Type: application/json; charset=utf-8
X-Requested-With: XMLHttpRequest
Referer: http://[redacted]/
Content-Length: 76
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache

{"type":1,"r1":90,"g1":10,"b1":0,"r2":20,"g2":20,"b2":30,"t1":500,"t2":1000}

For valve control:

POST /bloom/valve HTTP/1.1
Host: [redacted]
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:42.0) Gecko/20100101 Firefox/42.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
DNT: 1
Content-Type: application/json; charset=utf-8
X-Requested-With: XMLHttpRequest
Referer: http://[redacted]/
Content-Length: 24
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache

{"valve":1,"inverter":1}

At this point, my dest device is sitting on a rooted Linksys WRT54GL wireless network. Inspecting the traffic is trivial from this position within the network. Even more favorably, there is no encryption between the device and the cloud-based API. As far as the type of information within the network traffic, it's mostly non-sensitive information pertaining to watering schedules, current weather information, etc. However, it does also contain the approximate GPS coordinates and physical address for the device. In my opinion, this traffic probably should be encrypted. I used fake information but have redacted the data shown here so as to not implicate unaffiliated third parties. You know ...just in case.

HTTP/1.1 200 OK
Allow: GET, POST, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Date: Thu, 19 Nov 2015 22:58:38 GMT
Vary: Accept
transfer-encoding: chunked
Connection: keep-alive

{"latitude": [redacted], "longitude": [redacted], "zones": [{"id": 44318, "name": "Zone 1", "plant_type": 1,
 "emitter_type": 5, "gets_rainfall": true, "user_watering_modulation": 1.0, "valve": 1, "active": true,
 "illustration": null, "thumbnail_1": null, "thumbnail_2": null, "thumbnail_3": null, "thumbnail_4": null,
 "schedule_manual_secs": 180, "auto_scheduling": true, "precipitation_rate": 16.51, "kc": 0.75},
 {"id": 44319, "name": "Zone 2", "plant_type": 1, "emitter_type": 5, "gets_rainfall": true,
 "user_watering_modulation": 1.0, "valve": 2, "active": true, "illustration": null, "thumbnail_1": null,
 "thumbnail_2": null, "thumbnail_3": null, "thumbnail_4": null, "schedule_manual_secs": 180,
 "auto_scheduling": true, "precipitation_rate": 16.51, "kc": 0.75}, {"id": 44320, "name": "Zone 3",
 "plant_type": 1, "emitter_type": 5, "gets_rainfall": true, "user_watering_modulation": 1.0, "valve": 3,
 "active": true, "illustration": null, "thumbnail_1": null, "thumbnail_2": null, "thumbnail_3": null,
 "thumbnail_4": null, "schedule_manual_secs": 180, "auto_scheduling": true, "precipitation_rate": 16.51,
 "kc": 0.75}, {"id": 44321, "name": "Zone 4", "plant_type": 1, "emitter_type": 5, "gets_rainfall": true,
 "user_watering_modulation": 1.0, "valve": 4, "active": false, "illustration": null, "thumbnail_1": null,
 "thumbnail_2": null, "thumbnail_3": null, "thumbnail_4": null, "schedule_manual_secs": 180,
 "auto_scheduling": true, "precipitation_rate": 16.51, "kc": 0.75}, {"id": 44322, "name": "Zone 5",
 "plant_type": 1, "emitter_type": 5, "gets_rainfall": true, "user_watering_modulation": 1.0, "valve": 5,
 "active": false, "illustration": null, "thumbnail_1": null, "thumbnail_2": null, "thumbnail_3": null,
 "thumbnail_4": null, "schedule_manual_secs": 180, "auto_scheduling": true, "precipitation_rate": 16.51,
 "kc": 0.75}, {"id": 44323, "name": "Zone 6", "plant_type": 1, "emitter_type": 5, "gets_rainfall": true,
 "user_watering_modulation": 1.0, "valve": 6, "active": false, "illustration": null, "thumbnail_1": null,
 "thumbnail_2": null, "thumbnail_3": null, "thumbnail_4": null, "schedule_manual_secs": 180,
 "auto_scheduling": true, "precipitation_rate": 16.51, "kc": 0.75}, {"id": 44324, "name": "Zone 7",
 "plant_type": 1, "emitter_type": 5, "gets_rainfall": true, "user_watering_modulation": 1.0, "valve": 7,
 "active": false, "illustration": null, "thumbnail_1": null, "thumbnail_2": null, "thumbnail_3": null,
 "thumbnail_4": null, "schedule_manual_secs": 180, "auto_scheduling": true, "precipitation_rate": 16.51,
 "kc": 0.75}, {"id": 44325, "name": "Zone 8", "plant_type": 1, "emitter_type": 5, "gets_rainfall": true,
 "user_watering_modulation": 1.0, "valve": 8, "active": true, "illustration": null, "thumbnail_1": null,
 "thumbnail_2": null, "thumbnail_3": null, "thumbnail_4": null, "schedule_manual_secs": 180,
 "auto_scheduling": true, "precipitation_rate": 16.51, "kc": 0.75}, {"id": 44326, "name": "Zone 9",
 "plant_type": 1, "emitter_type": 5, "gets_rainfall": true, "user_watering_modulation": 1.0, "valve": 9,
 "active": true, "illustration": null, "thumbnail_1": null, "thumbnail_2": null, "thumbnail_3": null,
 "thumbnail_4": null, "schedule_manual_secs": 180, "auto_scheduling": true, "precipitation_rate": 16.51,
 "kc": 0.75}, {"id": 44327, "name": "Zone 10", "plant_type": 1, "emitter_type": 5, "gets_rainfall": true,
 "user_watering_modulation": 1.0, "valve": 10, "active": true, "illustration": null, "thumbnail_1": null,
 "thumbnail_2": null, "thumbnail_3": null, "thumbnail_4": null, "schedule_manual_secs": 180,
 "auto_scheduling": true, "precipitation_rate": 16.51, "kc": 0.75}, {"id": 44328, "name": "Zone 11",
 "plant_type": 1, "emitter_type": 5, "gets_rainfall": true, "user_watering_modulation": 1.0, "valve": 11,
 "active": true, "illustration": null, "thumbnail_1": null, "thumbnail_2": null, "thumbnail_3": null,
 "thumbnail_4": null, "schedule_manual_secs": 180, "auto_scheduling": true, "precipitation_rate": 16.51,
 "kc": 0.75}, {"id": 44329, "name": "Zone 12", "plant_type": 1, "emitter_type": 5, "gets_rainfall": true,
 "user_watering_modulation": 1.0, "valve": 12, "active": true, "illustration": null, "thumbnail_1": null,
 "thumbnail_2": null, "thumbnail_3": null, "thumbnail_4": null, "schedule_manual_secs": 180,
 "auto_scheduling": true, "precipitation_rate": 16.51, "kc": 0.75}], "channel": "channel_[redacted]",
 "sch_start": "+60", "address": {"city": "[redacted]", "country": "[redacted]", "street2": null,
 "zipcode": "[redacted]", "state": "[redacted]", "street": "[redacted]"}, "timezone": "[redacted]",
 "current_time": "2015-11-19T14:58:37.925-08:00", "avg_eto": 1.74}

Anyhow, I am at a cross-road. There are two ways I can go about accomplishing our goal. I got close to making both work, but in the end and for the purpose of this blog, I will only discuss one. The first way, and way I won't go into detail on how to recreate, has code to almost completely rebuild their cloud-based API, including the phone app. The phone app has a button that lets you run the sprinklers arbitrarily. This is why it was my intial target for accomplishing our goal. In the end, that route was taking much more effort than what it would for the route I'll talk about later on. A bit of information on how their cloud architecture works:

I thought it would work kind of like this:

[Phone App] -> [Cloud API] <-> [Blossom]

But really it works something like this:

[Phone App] -> [Blossom API] <-> [Blossom]
                                     ^
                                     |
                  [PubNub API] <-----+

PubNub is a realtime messaging service and content delivery network. The Blossom device makes thousands of HTTP calls per day, which can be difficult to scale and can probably create debugging problems when attempting to triage customer issues. PubNub likely helps with that by integrating code into the device that allows a more granular way of tracking device / user pairing and network state. PubNub returns a job identifer along with an EPOCH time that I believe is correlated to the job scheduled through the phone app.

There are big reasons why you want to remove third-parties from your networks. One such example can be found in the Privacy Policy used by the manufacturer of the Blossom. The manufacturer is owned by a company called iConservo and the policy in full can be found at: http://myblossom.com/legal/iconservo-privacy-statement/

What We Collect

[snip]
We also collect passive information such as your IP address on certain
IConservo Products, your ZIP code, as well as information about your
IConsevo Product such as MAC addresses, product model numbers,
software versions, chipset IDs, and region and language settings.
Passive information also includes information about the products you
request or purchase, the presence of other devices connected to your
local network, and the number of users and frequency of use of
IConservo Products and Services. We also collect passive information
regarding customer activities on our website. Some passive information
may be associated with personally identifying information.  [snip]

Who We Share With

We work with third parties in connection with some aspects of the
ICONSERVO Products and Services, such as but not limited to processing
payments and providing marketing assistance. 
[snip]

I'll just quote this again to make sure it's read: "the presence of other devices connected to your local network".

I will let the readers individually infer what this legal text means in relation to what the Blossom will collect about you and devices on your network.

If you want to retain the cloud-based functionality, null routing PubNub will not be an option as a HTTP response from PubNub is what seems to actually start the watering cycle scheduled through the phone app. There is another way to open and close valves manually through the web portal as well.

Blossom communicates with two internet accessible API servers: *.myblossom.com and *.pubnub.com

How do I take control of the network traffic? Just like a sandnet for malware analysis. Set up DHCP and DNS. Configure DHCP to issue the DNS server along with the lease. Next, configure DNS with records to return internal IP addresses for the aforementioned API servers. Blossom will honor this configuration.

The redacted content contains the local IP address for my Blossom device on the testing network and a pairing code to associate the device to my demo account. A sample of the traffic to *.myblossom.com can be found below.

[redacted] - - [28/Nov/2015 08:21:38] "GET /api/device/v1/server/?q=0.8.1035&fs=0.0.9&c=[redacted]&n=p HTTP/1.1" 404 -
[redacted] - - [28/Nov/2015 08:21:53] "GET /api/device/v1/server/?q=0.8.1035&fs=0.0.9&c=[redacted]&n=p HTTP/1.1" 404 -
[redacted] - - [28/Nov/2015 08:22:08] "GET /api/device/v1/server/?q=0.8.1035&fs=0.0.9&c=[redacted]&n=p HTTP/1.1" 404 -

I noticed that if the device is unable to establish communication with the API servers, it will turn the display LED red indicating connectivity failure and eventually purple indicating the start of an automated recovery process. If connectivity is not restored on the subsequent boot, the recovery process will continue indefinitely.

On to the second option, which is more or less a derivative of the first. I am going to trick the Blossom device into staying online. Next, I'll track our watering schedule and issue commands to open and close each valve I care about. In this way, I will remove the need for both the internet-facing Blossom API and PubNub API while retaining the ability to water my lawn when I want. Ok, here I go.....

Upon boot, the device performs a variety of requests to prepare for operation.

Upon resolution of these names the Blossom will then make three HTTP requests.

[redacted] - - [28/Nov/2015 09:12:46] "GET /firmware-check/0.json?q=0.8.1035&c=[redacted] HTTP/1.1" 404 -
[redacted] - - [28/Nov/2015 09:12:48] "GET /api/device/v1/server/?q=0.8.1035&fs=0.0.9&c=[redacted]&n=w HTTP/1.1" 200 -
[redacted] - - [28/Nov/2015 09:12:55] "GET /device/v1/server/ HTTP/1.1" 200 -

The first is a firmware check which seems to occur at every boot and every 3600 seconds thereafter. The frequency of this check can be changed using the ota_freq variable in the do_GET function of the Handler class within the code for this blog post. If the device receives a 404 HTTP responce code, it moves along. The second and third requests are what keep the device online. These requests must receive a properly constructed response. This is easy enough, just follow their JSON format. The request for the firmware returns a 404 HTTP response code with no JSON structure, I'll skip that one.

GET /api/device/v1/server/?q=0.8.1035&fs=0.0.9&c=[redacted]&n=w HTTP/1.1
Host: home.myblossom.com
User-Agent: WMSDK

HTTP/1.1 200 OK
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Date: Fri, 20 Nov 2015 15:32:32 GMT
Vary: Accept, Cookie
Content-Length: 88
Connection: keep-alive

{"ts": "2015-11-20T07:32:32.259-08:00", "pst_tz_offset": -8.0, "pst_tz_seconds": -28800}
GET /device/v1/server/ HTTP/1.1
Host: home.myblossom.com
User-Agent: WMSDK

HTTP/1.1 200 OK
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Date: Fri, 20 Nov 2015 15:32:32 GMT
Vary: Accept, Cookie
Content-Length: 88
Connection: keep-alive

{"ts": "2015-11-20T07:32:41.212-08:00", "pst_tz_offset": -8.0, "pst_tz_seconds": -28800}

You'll know everything is going smoothly when the LED on the front display remains a solid green throughout the duration of device uptime.

How about making the Blossom water what I want and when I want it? I'm glad you asked! A few notes first: If you want to make changes to your watering cycle in the future, you'll have to stop the API and make the those changes in code first. If you want to change the zones to be watered in each cycle, you'll have to stop the API and make the changes in code first. The same thought process can be used for zone watering durations. That's okay though, it keeps the mind limber! Lets get down to it.

The below code is annotated with ^""" blocks; the script is also available without the extra annotations: blossom-py3.py

#!/usr/bin/env python3

"""
First, I need a bunch of imports:
"""

from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.request import Request, urlopen
from time import strftime, sleep
from datetime import datetime
from threading import Thread
from json import dumps
from sys import exit

"""
I'll turn off debug, you can enable it later if you want an additional
logging statement.
"""

debug = 0

"""
I'll also create some code to log when valves are opened and closed
and recreate a python 2.7 feature that I needed when trying to quickly
port this over.
"""

def xrange(low,high):
    ''' Recreates the xrange I love from the 2.7 version of Python. '''
    return iter(range(low,high))

def log_it(valve, state):
    ''' Creates a simple log entry for each operation.  '''
    fd = open('blossom.log', 'a+')
    if (state == 1):
	    fd.write("{:s} -> Opened valve: {:s}\n".format(str(strftime("%Y-%m-%dT%H:%M:%S")), str(valve)))
    else:
        fd.write("{:s} -> Closed valve: {:s}\n".format(str(strftime("%Y-%m-%dT%H:%M:%S")), str(valve)))
    fd.close()
    return

"""
I'll need a class to tell Blossom to open and close our desired
valves:
"""

class Blossom:
    ''' Instructs the Blossom to perform a desired operation. '''

"""
The IP address of the Blossom device should be put in the self.apiHost
variable.
"""

    def __init__(self):
        ''' Defines a shared variable. '''
        self.apiHost = "[redacted]"  # IP Address of Blossom
        return

"""
To open a valve, I simply send a POST request to the Blossom to
/bloom/valve with a JSON object containing two keys: valve and
inverter.  The valve indicates which zone you want to run, and
inverter is a boolean (either 0 or 1) indicating what state the valve
should be in.
"""

    def open_valve(self, valveNumber):
        ''' Instructs Blossom to open a valve. '''
        request = Request("http://{:s}/bloom/valve".format(str(self.apiHost)), dumps({"valve":valveNumber,"inverter":1}).encode('ascii'))
        fd = urlopen(request)
        return fd.close()

"""
The same goes for closing a valve, only the inverter value has been
changed.
"""

    def close_valve(self, valveNumber):
        ''' Instructs Blossom to close a valve. '''
        request = Request("http://{:s}/bloom/valve".format(str(self.apiHost)), dumps({"valve":valveNumber,"inverter":0}).encode('ascii'))
        fd = urlopen(request)
        return fd.close()


"""
Now I need a class to handle HTTP requests, I'll call it Handler. I
need to define the UUID for our Blossom within this class as well.
"""

class Handler(BaseHTTPRequestHandler):
    ''' Decides how to handle each request. '''

"""
A function called do_GET is used to handle each received GET request.
"""

    def do_GET(self):

"""
I do some simple request inspection to decide how to respond. I'll
also set some headers for the response.
"""

        ''' In the case of HTTP GET, the request type of checked and an appropriate response is returned. '''
        uuid = "[redacted]"  # Blossom UUID goes here
        if ("firmware-check" not in self.path):
            self.send_response(200)
            self.send_header('Content-Type', 'application/json')
            self.send_header('Vary', 'Accept')
            self.send_header('Connection', 'Close')
            self.end_headers()
        else:
            self.send_response(404)
            self.end_headers()

"""
This is where the API will decide how to respond based on the received
request.  The self.path variable contains the URI as it was received
by the web server. I use this to determine how to build the proper
response. The self.wfile.write function calls are used to actually
build the response. The response gets delivered to the client upon
return.

The first URI is /device/v1/server/ which is more-or-less a server
ping. The response contains time information.
"""

        if (self.path == '/device/v1/server/'):
            self.wfile.write(str('{"ts": "{:s}", "pst_tz_offset": -8.0, "pst_tz_seconds": -28800}'.format(str(strftime("%Y-%m-%dT%H:%M:%S")))))

"""
The second is /api/device/v1/ and looks for an additional URI value
called parameter.
"""

        elif ("api" in self.path and "device" in self.path and uuid in self.path):

"""
If the URI contains parameters,
"""

            if ("api" in self.path and "parameters" in self.path):
                self.wfile.write(str('{"stats_freq": 3600, "pn_keepalive": 1, "uap_debug": 1, "wave_boost": 1, "ota_freq": 3600, "current_time":"{:s}", "build": 1042, "opn_trip": 40}'.format(str(strftime("%Y-%m-%dT%H:%M:%S")))))

"""
Otherwise,
"""

            else:
                self.wfile.write(str('{"channel": "channel_{:s}", "current_time": "{:s}", "tz_offset": -8.0, "tz_seconds": -28800, "sch_load_time": 24900, "fetch_lead": 3600}'.format(str(uuid), str(strftime("%Y-%m-%dT%H:%M:%S")))))

"""
And finally, our return statement.
"""

        return

"""
Now for some code to manage the watering schedule.
"""

class Monitor:
    ''' Monitors the current datetime and watering schedule.  '''

"""
I'll need to define a few variables for use later. The variables
*_scheduleDays indicate what days you want your lawn to be watered.
The variables *_scheduleTimes indicate what time of day you want your
lawn watered. The *_zones variable indicates which zones you want
watered and for how long in seconds you want each valve to be open.
"""

    def __init__(self):
        self.front_scheduleDays = ['Tue', 'Thu', 'Sat']
        self.front_scheduleTimes = [400, 2330]
        self.front_zones = {'zones': [1, 2, 3], 'water_times': [60, 60, 120]}
        self.back_scheduleDays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri']
        self.back_scheduleTimes = [530, 2355]
        self.back_zones = {'zones': [8, 9, 10, 11, 12], 'water_times': [30, 30, 30, 30, 0]}
        self.days = {'Mon': 0, 'Tue': 1, 'Wed': 2, 'Thu': 3, 'Fri': 4, 'Sat': 5, 'Sun': 6}
        return

"""
Now I need to start a infinite loop to contintually check when to
water.
"""

    def run(self):
        ''' Continually checks the current datetime and decides whether or not to water. '''
        print(datetime.now(), " -> Starting")
        while True:
            print(datetime.now(), " -> Checking schedule")

"""
I start by getting the current day of the week and time.
"""

            self.current_day = datetime.today().weekday()
            self.current_time = int(datetime.now().strftime('%H%M'))

"""
Next, I decide whether or not self.current_day is a day of the week
that I want to water my lawn.  This is for the front yard, I'll repeat
for the back yard as well.
"""

            for schedule_day in self.front_scheduleDays:
                if self.days[schedule_day] is self.current_day:
                    for schedule_time in self.front_scheduleTimes:
                        if debug == 1: print(datetime.now(), " -> [F] Checking if ", self.current_time, " is in ", xrange(schedule_time-1, schedule_time+1))

"""
Next, I decide whether or not self.current_time is within the time
window of when I want to water my lawn. If I use a sleep of thirty
seconds at the end of each loop iteration, assuming self.current_time
walls within the window alloted (+/- 1 minute) and you water two zones
at a minimum of thirty seconds each, there shouldn't be any issues.
This isn't the best way of doing this, but it works satisfactorily.
"""

                        if self.current_time in xrange(schedule_time-1, schedule_time+1):

"""
Finally, I can begin to iterate through our zones to be watered and
open the valves for each.
"""

                            for zone_info in zip(self.front_zones['zones'], self.front_zones['water_times']):
                                if (zone_info[1] > 0):

"""
Open the valve.
"""

                                    print(datetime.now(), " -> [F] Opening valve: ", zone_info[0])
                                    log_it(zone_info[0], 1)
                                    Blossom().open_valve(zone_info[0])

"""
Make it rain!
"""

                                    sleep(zone_info[1])

"""
Remember to close the valve. If you don't, you'll be watering until
you do.
"""

                                    print(datetime.now(), " -> [F] Closing valve: ", zone_info[0])
                                    log_it(zone_info[0], 0)
                                    Blossom().close_valve(zone_info[0])

"""
Rinse and repeat with the back yard.
"""

            for schedule_day in self.back_scheduleDays:
                if self.days[schedule_day] is self.current_day:
                    for schedule_time in self.back_scheduleTimes:
                        if debug == 1: print(datetime.now(), " -> [B] Checking if ", self.current_time, " is in ", xrange(schedule_time-1, schedule_time+1))
                        if self.current_time in xrange(schedule_time-1, schedule_time+1):
                            for zone_info in zip(self.back_zones['zones'], self.back_zones['water_times']):
                                if (zone_info[1] > 0):
                                    print(datetime.now(), " -> [B] Opening valve: ", zone_info[0])
                                    log_it(zone_info[0], 1)
                                    Blossom().open_valve(zone_info[0])
                                    sleep(zone_info[1])
                                    print(datetime.now(), " -> [B] Closing valve: ", zone_info[0])
                                    log_it(zone_info[0], 0)
                                    Blossom().close_valve(zone_info[1])

"""
Sleep at the end of each iteration.
"""

            sleep(30)

"""
Finally, return to the caller.
"""

        return


"""
I also need a main routine to get things going.
"""

def main():
    ''' Spawn a thread for datetime monitoring. Serve the API. '''

"""
I'll use two threads. The parent will run the web server, and a child
thread will monitor the schedule and instruct Blossom on operations to
be performed.
"""

    Thread(name='monitor-schedule', target=Monitor().run, args=()).start()

"""
Now I'll start the API.
"""

    try:
        server = HTTPServer(('', 81), Handler)
        server.serve_forever()
    except KeyboardInterrupt:
        print("^C received, shutting down the web server")
        server.socket.close()
    return exit(1)

"""
Run main.
"""

if __name__ == "__main__":
	main()

I hope you enjoyed reading this blog, I had fun writing it. I've had this configuration deployed for a few weeks now, and so far, it's working pretty well. Next week, I'll be talking about an undocumented account in an IoT device.


0 comments Posted by Matt at: 17:45 permalink

Comments are closed for this story.