Home Automation System

Clayton Brutus, Computer Engineering

Project Advisor and Sponsor: Mr. Mark Randall

April 26, 2018

Evansville, Indiana

Table of Contents

I. Introduction

II. Background

III. Client Requirements

IV. Project Design A. Initial Design Choices B. Constraints and Considerations i. Safety Considerations ii. Mechanical Constraints iii. Manufacturability C. Hardware i. Relay Control ii. Light Dimming iii. Power Monitoring iv. Thermostat D. Software i. Server ii. Touchscreen Interface iii. Amazon Skill Lambda Function

V. Results

VI. Conclusion

VII. References

VIII. Appendices A. Schematic Print ​ B. Light Switch Bill of Materials C. Server Code D. Touchscreen GUI Code E. Amazon Skill Lambda Script

List of Figures

Figure 1: Digi XBee Pro S2C Development Module

Figure 2: Model 3 B

Figure 3: Amazon Echo Dot 2nd Edition

Figure 4: Lightswitch PCB Assembly

Figure 5: Hub Assembly

Figure 6: Relay Toggling Circuit

Figure 7: Light Dimming Circuit

Figure 8: Digital Potentiometer and PWM Generator Circuit

Figure 9: Diode AND Gate with Schmitt Trigger Inverter

Figure 10: Current Transformer

Figure 11: Power Usage Measuring Circuit

Figure 12: 3D PCB Renderings (Front, Back)

Figure 13: Server Request Processing Flowchart

Figure 14: Touchscreen Interface

List of Tables

Table 1: Existing System

Table 2: Available Voice Commands

I. Introduction

Home automation systems are a relatively new concept which aim to make houses more intelligent and automated in order to make life easier and safer for a house’s occupants. Existing systems usually are capable of controlling the lighting, appliances, temperature, and security systems of an home by replacing the devices in the traditional ‘dumb’ devices with advanced

‘smart’ devices. These devices are usually controllable by the user through numerous communication channels such as smartphone applications, voice assistants (e.g. Amazon Alexa,

Google Assistant), and pre-programmed schedules. Home automation systems not only provide a great convenience to its users, but also serve to increase the productivity and safety. The global market for home automation systems is expected to grow from $35.24 billion in 2016 to $113.82 billion by 2025 at a compound annual growth rate of 13.93% between 2017 and 2025 [1]. Soon home automation systems of some form will be in most people’s homes, which is why almost every major consumer electronics company has released devices for home automation.

II. Background

Existing home automation systems made by many companies are available at most electronics stores. Each home automation system typically consists of a hub of some sort which serves to connect the different communication channels to the automation system, coordinates communication to/from and between the devices, and runs tasks scheduled by the user. The devices such as a light switch, outlet, or thermostat can be purchased separately from the hub and any number of those devices may be added to the system as needed by the user. The hub typically communicates with the peripheral devices through one of these communication

standards: Wi-Fi (IEEE 802.11), Z-Wave, ZigBee, and LE (low energy). The key differences in these standards are in range/network topography, power usage, and cost. All of these technologies have a comparable indoor range that is enough for most houses. Wi-Fi is the only one of these technologies that does not support a mesh network topography, which puts it behind the others because it may not work as well for larger houses or houses with large obstacles. The others’ mesh topography allows each device to be a repeater such that the optimal route to each device is found. Z-Wave, ZigBee, and Bluetooth LE also require much less power to operate compared to Wi-Fi. There is not a clear difference between these three technologies when only comparing their capabilities. The main difference between Z-Wave and ZigBee is that

ZigBee is an open standard based on the IEEE 802.15.4 wireless-data specification while

Z-Wave is a proprietary standard. Because of that, ZigBee is cheaper to utilize in devices and is less restricted in its usage. Bluetooth LE with mesh support was only released in July 2017, so it is relatively new and unused [2].

An example of a home automation system that can be built using existing components is shown in Table 1 and as follows: Samsung SmartThings ZigBee Enabled Hub ($99.99), Amazon

Echo Dot ($49.99), Honeywell Touchscreen Wifi Thermostat ($206.99), GE ZigBee Wireless

Smart Appliance Switch ($49.99), and GE In-Wall ZigBee Smart Lighting Control Dimmer

($54.99) for a total cost of $461.95. This system would be capable of controlling a single outlet

(for any appliance or light), a single light switch, and the thermostat while also monitoring the power usage of each device controlled by the system. This relatively small initial system will cost a large amount of money and will then cost $49.99 or $54.99 for each additional outlet or light device added to the system. In order to fully automate a whole house, the cost would be

unbearable for most people. On top of the large cost, this system does not allow the user to tweak the inner workings of the system, nor does it allow them to add custom devices.

Table 1: Existing Home Automation System

Description Cost

Samsung SmartThings ZigBee Enabled Hub $99.99

Amazon Echo Dot $49.99

Honeywell Touchscreen Wifi Thermostat $206.99

GE ZigBee Wireless Smart Appliance Switch $49.99

GE In-Wall ZigBee Smart Lighting Control Dimmer $54.99

Total $461.95

This project aims to introduce a new home automation system to the market which solves the issues of high cost and low transparency / customizability of existing systems while retaining advanced features such as power monitoring, light dimming, scheduling, and ease of use. This project is a home automation system which allows the user to truly control what their automation system is doing, which should be a requirement for a system with such control over a person’s home.

III. Client Requirements

● Outlet switching for lights or appliances requiring up to 15A AC current (standard home

circuit breaker) [3]

● Lightswitch switching and dimming for incandescent and LED lights up to 15A AC

current

● Thermostat capable of controlling a standard home HVAC (heating, ventilation, and air

conditioning) system

● Power monitoring for all devices which are controlled by the system

● Compatibility with an existing voice assistant for voice control

● Touch screen interface for monitoring / controlling all of the devices connected to the

system

IV. Project Design

A. Initial Design Choices

In order for this system to be as transparent and cheap as possible, open source

standards, devices, and software must be used when possible so that there are no

proprietary aspects and no extra costs to the system. The best choice for wireless

communication protocol for communication between devices, given this project’s

requirements, is ZigBee since it is an open standard, is well established, and has many

development boards and software to make testing and deployment easier. The Digi XBee

Pro S2C Module (Figure 1) is the ZigBee development board used in this project because

it is one of the cheapest modules and provides niceties such as its XCTU software useful

for debugging and configuration of the devices, and there is also multiple open source programming APIs available for controlling the modules. These modules are relatively limited because they are not programmable, so they must be controlled remotely and themselves can not perform any autonomous tasks [4]. This is not an issue for this project since the hub will handle all of the ‘smart’ capabilities of the system while each device will only receive commands from the hub.

Figure 1: Digi XBee Pro S2C Development Module

The system’s hub, which functions similarly to existing home automation system hubs, serves to: bridge the gap between the ZigBee network and the user control interfaces, keep track of the devices currently in the system, track and run scheduled tasks, log the power usage of each device in the system. The hub consists of a Raspberry

Pi (shown in Figure 2), chosen for its small form factor, low cost, low power usage, and features which meet the requirements of this project. Those required features are: serial port for communication with a Digi XBee module, GPIO (general purpose input/output)

for controlling the relays required for HVAC system control, Wi-Fi capable for connecting the system to the internet, and touchscreen compatibility for the system’s touch screen interface [5]. Because the Raspberry Pi has the features needed for both the touch screen interface and the thermostat, it will serve as both the hub of the system and the thermostat in order to lower the overall cost of the system.

Figure 2: Raspberry Pi Model 3 B

The existing voice assistant chosen for compatibility with the system is the

Amazon Alexa, which runs on the Amazon Echo Dot. This voice assistant was chosen because of its relatively low cost ($49.99) and the ease of adding compatibility with other services such as this system. Amazon provides its Alexa Skills kit, which allows anyone to build ‘skills’ for Alexa to be compatible with other services [6].

Figure 3: Amazon Echo Dot 2nd Edition

B. Constraints and Considerations

i. Safety Considerations

Since this project is designed to be placed in a person’s home and meant to be running

24/7, certain safety constraints are necessary to ensure that the devices of this system are safe to use in a house. The most important safety consideration is the AC power, since the outlet and lightswitch devices are capable of handling up to 15A of AC current at 120V RMS. Therefore, it is important that the devices of the system which handle AC power are entirely enclosed and not easily accessible such that someone cannot easily shock themselves by handling and using the devices.

It is also important that the devices are actually able to handle their advertised current capability so that when that capability is put to the test, the device will not stop working, or, more importantly, cause damage or even a fire inside a person’s home. Therefore, each component of the PCBs and the PCBs themselves which handle AC power need to be able to handle 120VAC and 15A. There is also a fuse on the PCB rated for 15A which will trip if the

maximum current is exceeded so that no damage occurs to the components. The components and

PCB should also follow the guidelines in the IPC-2221B Generic Standard on Printed Board

Design, which is widely accepted as a generic PCB design standard for commercial and industrial applications. From that standard, the recommended minimum creepage between exterior AC power traces of 120V RMS is 1.25mm (49.2 mil) [7].

ii. Mechanical Constraints

The lightswitch device is very mechanically constrained since it is designed mount on/in a standard light switch gang enclosure which measures only 3 x 2 x 1 1/2" [8]. This makes the

PCB crowded and harder to place large components on the board. The hard part of the mechanical design is making the lightswitch PCB, which has many large components, fit on a standard light switch electrical box while keeping it small enough that it does not seem out of place when mounted on a wall in someone’s home. A 3D printed enclosure (shown in Figure 4) for the light switch PCB is needed to safely and securely mount the PCB on a wall and make it visually appealing.

Figure 4: Lightswitch PCB Assembly

iii. Manufacturability

Since low-cost is one of the main motivations behind this project, manufactuability must be highly considered since easily manufacturable devices will obviously be cheaper to make.

Luckily, the lightswitch and outlet devices of this project are similar enough that they can be made from a single PCB by simply adding some components to the outlet to get an outlet or removing components from the lightswitch to get an outlet PCB. The PCB for the hub

(thermostat circuitry) must be designed and manufactured separately (shown in Figure 5).

In order to lower PCB manufacturing costs, the PCBs can be panelized such that multiple devices can be made from a single PCB which are then cut out to form multiple boards. This will significantly lower the cost of each device.

Figure 5: Hub Assembly

C. Hardware

i. Relay Control

In order to switch the high voltage and current needed to power AC appliances and lights, a relay must be used. The relay must be rated for at least 15A since that is the current handling requirement decided above. In order for the user to be able to both switch the relay off/on remotely (via ZigBee communication) and manually (via a button press), the inputs must be or’ed together such that both can independently toggle the relay. This was accomplished with a

D-type flip-flop, which allows for a rising edge of the clock to switch the state of the output which controls the relay. Since a rising edge can be created easily using either a button or a

XBee GPIO pin, this solution works well for this application. The XBee module can poll the output of the flip flop to get the current status of the relay, and the user can look at the light / appliance to see whether pushing the button will turn the relay off or on. A mechanical relay requires more current than can be supplied by a logic-level D-type flip flop though so a

MOSFET must be used to drive the relay input. The problem with using a D-type flip flop in this way is that the initial state of the output is unknown and can be different from one turn-on to the next. This is a problem for this project because, for example the user first connects the device to power, or ,more consequently, if power is lost momentarily then restored then the device that the light switch or outlet PCB is powering might turn on which is obviously bad if that is not what the user desires. Therefore, the clear pin needs to be held low momentarily on turn-on in order to ensure that the output is initially off. That is the purpose of the RC circuit on the CLR# pin of the flip flop.

Figure 6: Relay Toggling Circuit

ii. Light Dimming

The much less straightforward part of the hardware design is the AC dimming circuit.

Researching methods of AC dimming using a microcontroller led me to use PWM (pulse width modulation) dimming because it is the only method which does not require a microcontroller to detect AC zero-crossings, which is not possible with a XBee module because it is not programmable. AC PWM dimming works similarly to the more common DC PWM dimming by turning a light off and on fast enough that it looks like it is on all the time. Reducing the on-time makes the light appear less bright, lowering the average brightness of the light. The circuit used in this project was created by using a circuit suggested in an online post as a reference [9].

Figure 7: Light Dimming Circuit

There is still a major problem though: the XBee module used does have any PWM outputs. The options then are to add a microcontroller which has PWM output that can poll inputs which are changed by the XBee GPIO in order to select a specific pulse width. That solution is not ideal since it involves adding a microcontroller which needs to be programmed and also limits the number of different pulse widths (and therefore brightness levels) because it would rely on only a few GPIO pins. An alternative and the solution chosen is to add a PWM generator chip which takes an analog input voltage and generates a different pulse width depending on that voltage. A potentiometer is used to control that voltage, but the voltage must be adjustable by both a user manually changing it and by the XBee which is remotely controlled.

Therefore a digital potentiometer must be used so that can be controlled by both input sources.

The XBee module has analog input pins so it can poll the digital potentiometer output to get the current brightness level of the light. For user input, a quadrature encoder must be used so that direction can be sensed in order to increase or decrease the digital potentiometer output level.

Digital potentiometers come in a couple forms, but the one selected operated simply by setting an Up / Down pin to high (up) or low (down) and then the digital potentiometer is moved on each rising edge of an Increment pin [10]. A D-type flip flop can be used to detect the direction of the encoder which will connect to the Up / Down pin, and then the other channel of the encoder is connected to the Increment pin. Since the XBee module must also be able to control the digital potentiometer, these signals must be OR’ed together so that neither the XBee or the encoder has sole control of the potentiometer. There is still the problem of one of the encoder

channels getting stuck high (in between notches) in which case the XBee module would be unable to drive the signals low when needed. Therefore, the signals from the encoder must be able to be turned off. This can be accomplished by clearing the D flip flop by driving its CLR# pin low with the XBee which will set the output low no matter what the input from the encoder is

[11]. The other channel of the encoder must be connected to the potentiometer through an AND gate so that the XBee can turn that signal off when the XBee needs control. To avoid adding

AND or OR chips to the board, diodes were used to create the necessary logic. The encoder channel outputs were not clean square waves, so in order to clean them up they are fed through a

Schmitt trigger inverter which has hysteresis.

Figure 8: Digital Potentiometer and PWM Generator Circuit

Figure 9: Diode AND Gate with Schmitt Trigger Inverter

iii. Power Monitoring

The approximate power usage of the devices powered by either a light switch or outlet is monitored using a current transformer. Current transformers go around an AC current carrying wire and produce a current on the secondary coil that is proportional to the current in the wire

[12].

Figure 10: Current Transformer

The current from the secondary coil is a 60Hz AC current, which changes too quickly for the XBee to read the signal level reliably since it can only be commanded to read the signal via remote command. Therefore, the current must be rectified and smoothed so that it is a DC voltage that is readable by the XBee module. The voltage must also be biased so that the voltage drop from the diode does not cause the measurement of smaller AC currents to be lost. The current transformer chosen is a 1000:1 transformer capable of measuring 15A AC which means that the maximum current coming out of the secondary coil is 15mA. The XBee module has an

ADC (Analog-to-Digital Converter) which can measure voltages from 0 to 1.2V, so a resistor in the secondary coil must be chosen such that the voltage on the circuit output never exceeds 1.2V after the diode [4]. This circuit is somewhat limiting because it can only measure the current amplitude, not the phase. This limitation means that this circuit can only truly measure purely resistive loads like incandescent light bulbs. For measuring the power consumption of non-resistive loads, the power factor must be approximated based on the type of device being powered so that a more accurate power measurement can be calculated.

Figure 11: Power Usage Measuring Circuit

Figure 12: 3D PCB Renderings (Front, Back)

D. Software

All software for this project is written in Python because of it’s XBee module API,

Raspberry Pi GPIO API, and my familiarity with the language. The software for this system is broken into several components: the hub, touchscreen user interface, and the Amazon Alexa

Skill Lambda script.

i. Server

The most important software aspect of the project is the software which runs on the

Raspberry Pi and is the backbone of the entire system. This server software must be able to receive requests from the user whether that be from the touchscreen or the Amazon Alexa. The server receives HTTP requests from these two sources. HTTP requests were chosen over other communication protocols because it can easily be processed in Python and is very versatile.

HTTP requests are versatile because they can be made by a large number of devices and services. For example, IFTTT (If This Then That) is a service used for connecting multiple services in order to automate tasks or trigger actions based on the location of a phone, for example. IFTTT may make HTTP requests, thus allowing IFTTT to be compatible with this system [12]. Also, HTTP requests can be URL encoded so that any device, which is capable of browsing the web, has the capability to make a request to the home automation server.

The server must also track the devices in the network, discover new devices, and run pre-programmed scheduled tasks. Figure 8 illustrates how the server processes requests. In the background, the server program is also periodically checking for new devices by sending discovery packets every 5 minutes and adding any discovered devices to the device database.

The server must also periodically check the power usage of each device and log the average power usage to a file for the user to examine when desired.

Figure 13: Server Request Processing Flowchart

ii. Touchscreen Interface

The touchscreen interface (shown in Figure 14) is designed to provide an intuitive way for the user to monitor and control the system. The software is written in python using a GUI library called Kivy. Kivy was chosen as the library to develop the touchscreen interface because it is for python, open source, cross-platform (Linux, Windows, and Android), and is designed for easy rapid development of user interfaces that utilize touchscreens. Kivy provides a large number of pre-build widgets which make it easy to make applications using it [14]. Although the touchscreen application runs on the Raspberry Pi where the server also runs, it is written as a completely separate application to the server application and makes http requests to the server.

Therefore, the GUI can run on a completely different device or even in a completely different location from the server. There could also be multiple touchscreens running the application at

once allowing the user to have multiple locations in their home from which they can control the system.

Figure 14: Touchscreen Interface

iii. Amazon Skill Lambda Function

Amazon Alexa Skills Kit allows developers to direct Alexa commands to Lambda function which can process the user’s command and respond to the user accordingly [15]. In the case of this project, the Lambda function serves to make a request (HTTP request) to the server running on the Raspberry Pi, which does the action requested by the user.

V. Results

The complete system functions as required by the client. The system is able to handle requests made by the user via the Amazon Echo, touch screen, and HTTP requests made by any other device or service (such as IFTTT).

Table 2: Available Voice Commands

Command Description Usage

Add Device Adds a new device to the device “Add

Remove Device Removes a device from the “Remove ” device database

Change Device Name Changes a device name in the “Change name from to

Set Device to Level Set a device to off or on “Set to (outlet/light), to a level in 0-100 ” (light)

Set Temperature Set thermostat desired “Set temperature to temperature

Get Temperature Get current thermostat “Get current temperature” temperature

Get Current Device Status Responds with status of device: “Get status of ” 0-100 (light), or temperature (thermostat)

Discover Devices Sends a discovery packet and “Discover devices” adds any found devices to the device database

VI. Conclusion

Although the system meets all of the requirements the system is not as refined as currently available home automation systems. The system is not as user friendly as existing products and the devices are not as visually appealing to the user. This project is a good start to a home automation system though, and it performs the necessary functions well. In order for this

system to be marketable it will need significant refinement and further development in software and hardware.

A. Future Work

There is a large amount of work that could be done to make the system more user

friendly or expand with different features. Currently, this system requires a lot more

technical knowledge to set up and use when compared to existing systems. Set up could

be made easier using a phone application, for example. Power measurement could also be

made more easily accessible by a GUI rather than by a csv file as it is currently.

Additional features could include: motion sensors and/or video camera

compatibility for home security purposes, IR remote control capability for controlling

devices such as TVs, receivers, etc., and many other things which would make the system

more useful in a person’s home.

VII. References

[1] “Home Automation Market to 2025 - Global Analysis and Forecasts,” PR Newswire: ​ news distribution, targeting and monitoring, 13-Nov-2017. [Online]. Available: ​ https://www.prnewswire.com/news-releases/home-automation-market-to-2025--global-analysis- and-forecasts-300554745.html

[2] K. Parrish, “ZigBee, Z-Wave, Thread and WeMo: What's the Difference?,” Tom's ​ Guide, 12-Dec-2017. [Online]. Available: ​ https://www.tomsguide.com/us/smart-home-wireless-network-primer,news-21085.html

[3] “How To Choose Circuit Breakers,” Choosing the Right Circuit Breakers at The ​ Home Depot. [Online]. Available: ​ https://www.homedepot.com/c/how_to_replace_circuit_breaker_HT_BG_EL

[4] “XBee/XBee-PRO ZB RF User’s Guide,” Digi.com. (2018). [online] Available at: https://www.digi.com/resources/documentation/digidocs/PDFs/90000976.pdf

[5] Raspberrypi.org. (2018). Raspberry Pi Hardware - Raspberry Pi Documentation. ​ ​ [online] Available at: https://www.raspberrypi.org/documentation/hardware/raspberrypi/README.md

[6] “Echo Dot (2nd Generation),” Amazon Echo Dot - Add Alexa to any room. [Online]. ​ ​ Available: https://www.amazon.com/dp/B01DFKC2SO/ ​

[7] Rozenblat, L. (2018). IPC-2221B PCB Trace Spacing / Clearance by Voltage. ​ ​ [online] Smpspowersupply.com. Available at: http://www.smpspowersupply.com/ipc2221pcbclearance.html

[8] Inspectapedia.com. (2018). Electrical Box Types & Sizes for Receptacles when wiring ​ receptacles (outlets) - How to choose the proper type of electrical box when wiring electrical receptacles. [online] Available at: ​ https://inspectapedia.com/electric/Electrical_Outlet_Box_Types.php

[9] Instructables.com. (2018). AC PWM Dimmer for . [online] Available at: ​ ​ http://www.instructables.com/id/AC-PWM-Dimmer-for-Arduino/

[10] DS4301 Digital Potentiometer Datasheet. (2018). [online] Available at: https://datasheets.maximintegrated.com/en/ds/DS4301.pdf

[11] SN74 D-Type Flip Flop Datasheet. (2018). [online] Available at: http://www.ti.com/lit/ds/symlink/sn74hct74.pdf

[12] Electronics Tutorials. (2018). Current Transformer Basics. [online] Available at: ​ ​ https://www.electronics-tutorials.ws/transformer/current-transformer.html

[13] Apscheduler.readthedocs.io. (2018). Advanced Python Scheduler — APScheduler ​ 3.5.0.post7 documentation. [online] Available at: https://apscheduler.readthedocs.io/en/latest/ ​ ​

[14] Kivy.org. (2018). Kivy: Cross-platform Python Framework for NUI. [online] ​ ​ Available at: https://kivy.org/ ​

[15] Developer.amazon.com. (2018). Host a Custom Skill as an AWS Lambda Function | ​ Custom Skills. [online] Available at: ​ https://developer.amazon.com/docs/custom-skills/host-a-custom-skill-as-an-aws-lambda-functio n.html

VIII. Appendices

A. Schematic Print

J1 1935161 17.5A 1 2 T5

Q2

3 T16 2 fout = 3985Hz = fout 4 4 COutput AC 1 R2 1M 1/10W R4 280K 1/10W 5V GND DIM_PWM T11 6 5 4 5V 5 6 4 V+ DIV OUT NC U3 MCT275 1 2 3 MOD SET GND U2 LTC6992CS6-2 1 3 2 GND 3 3 T1 R7 180 1/10W R3 196K 1/10W GND DIM_PWM GND 2 DPOT_OUT D2 GND 1 3 5 6 4 L H W D3 10V GND

R1 806K 1/10W R6 100K 1/4W

1 2 VCC INC# U/D# CS# 5V U1 DS4301

C1 47uF 200V 8 1 2 7

1 2 5V GND DPOT_INC# DPOT_U/D# 2 2 4 1 - + ~ ~ GND D1 GBU1506TB 15A 2 3 R5 10K 1/10W T4

Q1 3 2 5V GND T3 AC/N 1 2 1 T2 IN- IN+

C3 50V .1uF

1 1

1 2 AC2 AC2 AC1 AC1 RELAY_CTRL K1 RZ03-1A4-D005 16A C2 50V .1uF

5 6 3 4

1 2 5V GND eopigCapacitors Decoupling AC/L B C B C A D A D 6 8 10 12 U6C U6D U6E U6F 5 9 11 13 4 4 DPOT_INC# DPOT_U/D# R14 10K 1/10W R17 10K 1/10W GND GND 1 1 1 1 D5 D8 D9 D10 2 2 2 2 ubrRevision Number 4 XB_INC# ENC_U/D# XB_U/D# A4 Date: 3/11/2018 Sheet of Sheet By: Drawn Title Size 3/11/2018 C:\Users\..\Logic.SchDoc Date: File: U6B 3 C13 50V .1uF C11 .1uF 50V

GND 1 2

1 2 GND 2 7 3 3

C12 50V .1uF GND

1 2 5V GND VCC eopigCapacitors Decoupling U6A XB_CTRL# 5V 1 R18 10K 1/10W 14 GND 5V 5 6 4 1 R12 10K 1/10W 5V 1Q 1Q# 2 2 1PRE# 1CLR# 5V noe ieto Sensor Direction Encoder D6 D7 1 1 VCC 1D 1CLK GND U7A SN74HCT74DR R19 470K 1/10W 2 3 7 14 C14 50V .1uF

5V

GND RELAY_CTRL 1 2 GND ENC_A XB_CTRL# ENC_A ENC_B 2 2 9 8 10 13 2Q 2Q# 2PRE# 2CLR# R13 10K 1/10W 2D 2CLK ENC_BUT2 U7B SN74HCT74DR GND 11 12 5V

ENC_B ENC_A 5 R20 10K 1/10W ea Controller Relay

GND 4

C10 50V .1uF 3 R16 10K 1/10W

GND 1 2

5V

GND 2 1 1 1 J4 noe Connector Encoder C9 50V .1uF

D11 D12 R15 10K 1/10W

2 1 2 2 5V GND 1 1 N_ ENC_B ENC_A XB_RELAY_TOGGLE ENC_BUT2 B C B C A D A D

2 1

1 2

1 2

1 2

1 2

1 2

C. Server Code

1. #!/usr/bin/env python3 2. 3. import sys 4. import os 5. import json 6. from xbee import ZigBee 7. import serial 8. import logging 9. import time 10. from threading import * 11. from apscheduler.schedulers.background import BackgroundScheduler 12. from queue import * 13. 14. DEVICE_DB_FILENAME = "devices.json" # path to device db file 15. TASKS_DB_FILENAME = "sqlite:///tasks.db" # path to task db file 16. LOG_FILENAME = "thelog.log" # log filename 17. LOG_TIMESTAMP = "%Y-%m-%d %H:%M:%S" # timestamp format for logging 18. 19. SETUP_WAIT = 5 # time in seconds to wait for samples to be received on server startup 20. DISCOVERY_INTERVAL = 5 # time in minutes between network discovery packet sends 21. DISCOVERY_TASKID = "_discovery_task" # task id to use for network discovery task 22. 23. LEVEL_UNK = -1 # special device level used to mean level is unknown 24. 25. OUTLET_TYPE = "outlet" 26. LIGHT_TYPE = "light" 27. DEVICE_TYPES = [OUTLET_TYPE, LIGHT_TYPE] # valid device types 28. 29. DEFAULT_TIMEOUT = 5 # seconds 30. 31. WAIT_TIME = 0.1 32. 33. XB_CONF_HIGH = b'\x05' 34. XB_CONF_LOW = b'\x04' 35. XB_CONF_DINPUT = b'\x03' 36. XB_CONF_ADC = b'\x02' 37. 38. # relay toggle (toggles relay on a rising edge) 39. RELAY_TOGGLE = 'D0' 40. 41. # relay status 42. RELAY_STAT = 'D1' 43. RELAY_STAT_SAMPLE_IDENT = 'dio-1' 44. 45. # number of DPOT positions 46. DPOT_NUM_POS = 100 47. # DPOT INC# pin 48. DPOT_INC_N = 'D2' 49. # DPOT U/D# pin 50. DPOT_UD_N = 'D4' 51. # D flip flop clear (clears CS#) 52. DFLIPCLR_N = 'D5' 53. 54. # DPOT output pin 55. DPOT_OUT = 'D3' 56. DPOT_OUT_SAMPLE_IDENT = 'adc-3' 57. 58. class Home(): 59. def __init__(self, discover_function, task_function): 60. # setup logging 61. logging.basicConfig(filename=LOG_FILENAME, level=logging.DEBUG) 62. self.Log("starting server, please wait...") 63.

64. # setup task scheduler 65. self._sched = BackgroundScheduler() 66. self._sched.add_jobstore('sqlalchemy', url=TASKS_DB_FILENAME) 67. 68. # create lock for xbee access 69. self._zb_lock = RLock() 70. 71. # create lock for device_db access 72. self._db_lock = RLock() 73. 74. # create lock for permission to process packets 75. self._process_packets_lock = RLock() 76. 77. # create queue for holding pending packets 78. self._packet_queue = Queue(maxsize=10) 79. 80. # acquire locks 81. with self._zb_lock: 82. with self._db_lock: 83. 84. # setup connection to module 85. ser = serial.Serial() 86. ser.port = "/dev/ttyS0" 87. ser.baudrate = 9600 88. ser.timeout = 3 89. ser.write_timeout = 3 90. ser.exclusive = True 91. ser.open() 92. 93. self._ser = ser 94. 95. self._zb = ZigBee(ser, escaped=True, callback=self.Recv_handler) 96. 97. # load/create db file 98. if not (os.path.isfile(DEVICE_DB_FILENAME)): # check if need to create db file 99. self.Log(DEVICE_DB_FILENAME + " file doesn't exist, creating it.") 100. self._device_db = dict() 101. # save db to file 102. self._Save_db() 103. 104. else: 105. with open(DEVICE_DB_FILENAME) as f: 106. self._device_db = json.load(f) 107. self.Log("opened existing db " + DEVICE_DB_FILENAME) 108. 109. # send discovery packet 110. self.Send_discovery_packet() 111. 112. # start scheduler 113. self._sched.start() 114. 115. # add network discovery task (self.Discover_devices) 116. self._sched.add_job(self.Send_discovery_packet, trigger='interval', minutes=DISCOVERY_INTERVAL, replace_existing=True, id=DISCOVERY_TASKID) 117. 118. # store task_function for using when adding tasks 119. self._task_function = task_function 120. 121. self.Log("server ready!") 122. 123. def _Save_db(self): 124. 125. # get db lock 126. with self._db_lock: 127. 128. # dump db to file

129. with open(DEVICE_DB_FILENAME, 'w') as f: 130. json.dump(self._device_db, f) 131. 132. """ 133. Function: Mac2bytes 134. receives a string of 16 hex characters (mac address) 135. returns a bytearray usable by ZigBee API 136. """ 137. def Mac2bytes(self, mac): 138. return bytearray.fromhex(mac) 139. 140. def Bytes2mac(self, mac): 141. return mac.hex() 142. 143. def Sample_device(self, device_name, timeout=DEFAULT_TIMEOUT, num_tries=3): 144. 145. # get db lock 146. with self._db_lock: 147. 148. # check if device in db 149. if(not self.Name_in_db(device_name)): 150. self.Log("cannot sample device \"" + device_name + "\", no device with that name in db") 151. # return unknown 152. return LEVEL_UNK 153. 154. # get device type 155. device_type = self._device_db[device_name]['type'] 156. # get device mac 157. bytes_mac = self.Mac2bytes(self._device_db[device_name]['mac']) 158. 159. # get process packets lock 160. with self._process_packets_lock: 161. 162. # clear the queue 163. while(not self._packet_queue.empty()): 164. self._packet_queue.get(block=False) 165. 166. # request sample (periodic sampling every 255 ms) 167. self._zb.remote_at(dest_addr_long=bytes_mac, command='IR', parameter=b'\x0FF'); 168. 169. for x in range(num_tries): 170. 171. # wait for a packet 172. packet = self._packet_queue.get(block=True, timeout=timeout) 173. 174. # check if didn't receive packet 175. if(not packet): 176. self.Log("could not get sample from device \"" + device_name + "\", check the device") 177. # turn off sampling 178. self._zb.remote_at(dest_addr_long=bytes_mac, command='IR', parameter=b'\x00'); 179. return LEVEL_UNK 180. 181. if("source_addr_long" not in packet): 182. self.Log("does not contain source addr") 183. continue 184. 185. # check if packet is from device of interest 186. if((bytearray(packet['source_addr_long'])) != bytes_mac): 187. self.Log("mac doesn't match device of interest") 188. continue 189. 190. # check if sample packet 191. if("samples" in packet): 192. samples = packet["samples"][0] 193. 194. # if outlet device

195. if(device_type == OUTLET_TYPE): 196. # get relay status 197. stat = samples[RELAY_STAT_SAMPLE_IDENT] 198. if(stat): 199. # turn off sampling 200. self._zb.remote_at(dest_addr_long=bytes_mac, command='IR', parameter=b'\x00'); 201. return 100 202. else: 203. # turn off sampling 204. self._zb.remote_at(dest_addr_long=bytes_mac, command='IR', parameter=b'\x00'); 205. return 0 206. 207. # if light device 208. elif(device_type == LIGHT_TYPE): 209. 210. # if contains relay status 211. if(RELAY_STAT_SAMPLE_IDENT in samples): 212. # get relay status 213. stat = samples[RELAY_STAT_SAMPLE_IDENT] 214. 215. # if relay is off 216. if(not stat): 217. # turn off sampling 218. self._zb.remote_at(dest_addr_long=bytes_mac, command='IR', parameter=b'\x00'); 219. return 0 220. 221. # if relay is on 222. else: 223. # if contains dpot analog out status 224. if(DPOT_OUT_SAMPLE_IDENT in samples): 225. 226. # get level 227. dpot_level = int(round(100*((samples[DPOT_OUT_SAMPLE_IDENT] / 1023.0) * 1.2))) 228. 229. # adjust the level 230. if(dpot_level >= 100): 231. dpot_level = 99 232. elif(dpot_level < 0): 233. dpot_level = 0 234. 235. # return level 236. # turn off sampling 237. self._zb.remote_at(dest_addr_long=bytes_mac, command='IR', parameter=b'\x00'); 238. return 100 - dpot_level 239. 240. # turn off sampling 241. self._zb.remote_at(dest_addr_long=bytes_mac, command='IR', parameter=b'\x00'); 242. return LEVEL_UNK 243. 244. """ 245. Function: Set_device_level 246. receives a device name and a level to set it to 247. returns True if successful, False otherwise 248. 249. level is an integer in the range [0, 100] from off to on 250. 251. valid levels for device types: 252. outlet : 0, 100 (off, on) 253. light : 0 - 100 (off - on) 254. thermostat : 0 - 100 (0 - 100 degrees fahrenheit) 255. """ 256. def Set_device_level(self, device_name, level): 257. 258. if(not self.Name_in_db(device_name)): 259. self.Log("could not set level of device \"" + device_name + "\", name not in db") 260. return False

261. 262. # get current device level 263. curr_level = self.Sample_device(device_name) 264. 265. # check if got a sample 266. if(curr_level == LEVEL_UNK): 267. self.Log("could not set device \"" + device_name +"\" level to " + str(level) + ", could not communicate with module") 268. return False 269. 270. # check if need to change the level 271. if(curr_level == level): 272. self.Log("did not need to set device \"" + device_name +"\" level to " + str(level) + ", was already set") 273. return True 274. 275. # get db lock 276. with self._db_lock: 277. 278. # get device type 279. device_type = self._device_db[device_name]['type'] 280. 281. # if outlet 282. if(device_type == OUTLET_TYPE): 283. # toggle relay 284. self._Toggle_relay(device_name) 285. return True 286. 287. elif(device_type == LIGHT_TYPE): 288. # set light level using a thread 289. Thread(target=lambda: self._Set_light(device_name, curr_level, level)).start() 290. return True 291. 292. def _Toggle_relay(self, device_name): 293. 294. # get db lock 295. with self._db_lock: 296. # get device mac 297. device_mac = self.Mac2bytes(self._device_db[device_name]['mac']) 298. 299. # get zigbee lock 300. with self._zb_lock: 301. # set relay toggle pin high 302. self._zb.remote_at(dest_addr_long=device_mac, command=RELAY_TOGGLE, parameter=XB_CONF_HIGH) 303. # wait 304. time.sleep(WAIT_TIME) 305. # make relay toggle pin low 306. self._zb.remote_at(dest_addr_long=device_mac, command=RELAY_TOGGLE, parameter=XB_CONF_LOW) 307. 308. def _Set_light(self, device_name, curr_level, level): 309. 310. # get db lock 311. with self._db_lock: 312. # get device mac 313. bytes_mac = self.Mac2bytes(self._device_db[device_name]['mac']) 314. 315. # get zigbee lock 316. with self._zb_lock: 317. 318. if(curr_level == 0): 319. # turn on the relay 320. self._Toggle_relay(device_name) 321. 322. curr_level = self.Sample_device(device_name) 323. 324. # set D flip flop CLR# to low (cleared) 325. self._zb.remote_at(dest_addr_long=bytes_mac, command=DFLIPCLR_N, parameter=XB_CONF_LOW) 326.

327. # if light is too dim 328. if(curr_level < level): 329. 330. # set U/D# to low (down) 331. self._zb.remote_at(dest_addr_long=bytes_mac, command=DPOT_UD_N, parameter=XB_CONF_LOW) 332. 333. # while the light is too dim 334. while(level - self.Sample_device(device_name) > 0): 335. # decrement the dpot 336. # set INC# high 337. self._zb.remote_at(dest_addr_long=bytes_mac, command=DPOT_INC_N, parameter=XB_CONF_HIGH) 338. # set INC# low 339. self._zb.remote_at(dest_addr_long=bytes_mac, command=DPOT_INC_N, parameter=XB_CONF_LOW) 340. 341. # light is too bright 342. else: 343. # set U/D# to high (up) 344. self._zb.remote_at(dest_addr_long=bytes_mac, command=DPOT_UD_N, parameter=XB_CONF_HIGH) 345. 346. # while the light is too bright 347. while(level - self.Sample_device(device_name) < 0): 348. # increment the dpot 349. self._zb.remote_at(dest_addr_long=bytes_mac, command=DPOT_INC_N, parameter=XB_CONF_HIGH) 350. # set INC# low 351. self._zb.remote_at(dest_addr_long=bytes_mac, command=DPOT_INC_N, parameter=XB_CONF_LOW) 352. 353. # set U/D# back to low 354. self._zb.remote_at(dest_addr_long=bytes_mac, command=DPOT_UD_N, parameter=XB_CONF_LOW) 355. 356. # set D flip flop CLR# to high (not cleared) 357. self._zb.remote_at(dest_addr_long=bytes_mac, command=DFLIPCLR_N, parameter=XB_CONF_HIGH) 358. 359. """ 360. Function: Name_in_db 361. given device name 362. returns true if device with that name is in db, false otherwise 363. """ 364. def Name_in_db(self, device_name): 365. # get db lock 366. with self._db_lock: 367. 368. for device in self._device_db: 369. if(device == device_name): 370. return True 371. 372. return False 373. 374. """ 375. Function: Mac_in_db 376. given device mac address (hex string or bytearray) 377. returns true if device with that mac address is in db, false otherwise 378. """ 379. def Mac_in_db(self, device_mac): 380. # get db lock 381. with self._db_lock: 382. 383. if(type(device_mac) is bytearray): 384. byte_format = True 385. else: 386. byte_format = False 387. 388. if(byte_format): 389. for device in self._device_db: 390. if(self.Mac2bytes(self._device_db[device]['mac']) == device_mac): 391. return True 392.

393. return False 394. 395. else: 396. for device in self._device_db: 397. if(self._device_db[device]['mac'] == device_mac): 398. return True 399. 400. return False 401. 402. """ 403. Function: Mac2name 404. given device mac address (hex string or bytearray) 405. returns name of device if in db, empty string ("") otherwise 406. """ 407. def Mac2name(self, mac): 408. # get db lock 409. with self._db_lock: 410. 411. if(type(mac) is bytearray): 412. for device_name in self._device_db: 413. if(self.Mac2bytes(self._device_db[device_name]['mac']) == mac): 414. return device_name 415. 416. else: 417. for device_name in self._device_db: 418. if(self._device_db[device_name]['mac'] == mac): 419. return device_name 420. 421. return False 422. 423. """ 424. Function: Add_device 425. attempts to add a device to the db, returns True if successful, false otherwise 426. """ 427. def Add_device(self, device_name, device_mac, device_type): 428. 429. # get db lock 430. with self._db_lock: 431. 432. self.Log("add device got lock") 433. 434. # check if device with that name or mac is already in db 435. if(self.Name_in_db(device_name)): 436. self.Log("there is already a device with name \"" + device_name + "\" in the db") 437. return False 438. elif(self.Mac_in_db(device_mac)): 439. self.Log("there is already a device with mac address \"" + device_mac + "\" in the db") 440. return False 441. 442. # check if invalid device type 443. if(device_type not in DEVICE_TYPES): 444. self.Log("invalid device type \"" + device_type + "\", cannot add to db") 445. return False 446. 447. # get mac as bytes 448. bytes_mac = self.Mac2bytes(device_mac) 449. 450. # for outlets and lights, set up change detection for input button 451. if(device_type in [OUTLET_TYPE, LIGHT_TYPE]): 452. # set RELAY_STATUS (D1) to input 453. self._zb.remote_at(dest_addr_long=bytes_mac, command=RELAY_STAT, parameter=XB_CONF_DINPUT) 454. 455. # set RELAY_TOGGLE (D0) to output low 456. self._zb.remote_at(dest_addr_long=bytes_mac, command=RELAY_TOGGLE, parameter=XB_CONF_LOW) 457. 458. if(device_type == LIGHT_TYPE):

459. # set DPOT_OUT (D2) to analog input 460. self._zb.remote_at(dest_addr_long=bytes_mac, command=DPOT_OUT, parameter=XB_CONF_ADC) 461. 462. # set D flip flop CLR# to high 463. self._zb.remote_at(dest_addr_long=bytes_mac, command=DFLIPCLR_N, parameter=XB_CONF_HIGH) 464. 465. # DPOT INC# to low 466. self._zb.remote_at(dest_addr_long=bytes_mac, command=DPOT_INC_N, parameter=XB_CONF_LOW) 467. 468. # set U/D# to low 469. self._zb.remote_at(dest_addr_long=bytes_mac, command=DPOT_UD_N, parameter=XB_CONF_LOW) 470. 471. # create node identifier 472. node_identifier = device_type + "-" + device_mac[12:] 473. 474. # write node identifier to device 475. self._zb.remote_at(dest_addr_long=bytes_mac, command='NI', parameter=node_identifier) 476. 477. # apply changes 478. self._zb.remote_at(dest_addr_long=bytes_mac, command='AC') 479. # save configuration 480. self._zb.remote_at(dest_addr_long=bytes_mac, command='WR') 481. 482. # add to db dict 483. self._device_db[device_name] = {'name':device_name, 'mac':device_mac, 'type':device_type} 484. 485. # update db file 486. self._Save_db() 487. 488. self.Log("added device \"" + device_name + "\" of type \"" + device_type + "\" to db") 489. return True 490. 491. """ 492. Function: Remove_device 493. attempts to remove a device from the db, returns True if successful, false otherwise 494. """ 495. def Remove_device(self, device_name): 496. # get db lock 497. with self._db_lock: 498. 499. # check if device with that name or mac is already in db 500. if(not self.Name_in_db(device_name)): 501. self.Log("could not remove device \"" + device_name + "\" from db, no device with that name exists") 502. return False 503. 504. # remove from db 505. del(self._device_db[device_name]) 506. 507. # update db file 508. self._Save_db() 509. 510. self.Log("removed device \"" + device_name + "\" from db") 511. return True 512. 513. 514. """ 515. Function: Change_device_name 516. given old name and new name, changes device name 517. returns True if successful, false otherwise 518. """ 519. def Change_device_name(self, orig_name, new_name): 520. # get db lock 521. with self._db_lock: 522. 523. # check if device with that name is in db 524. if(not self.Name_in_db(orig_name)):

525. self.Log("could not rename device called \"" + orig_name + "\" from the db, no device with that name exists") 526. return False 527. 528. # check if new name already in db 529. if(self.Name_in_db(new_name)): 530. self.Log("could not change name to \"" + new_name + "\", device with name already in db") 531. return False 532. 533. # save device 534. saved_device = self._device_db[orig_name] 535. 536. # remove old device name from db 537. del(self._device_db[orig_name]) 538. 539. # add new device name to db 540. self._device_db[new_name] = saved_device 541. 542. # update db file 543. self._Save_db() 544. 545. self.Log("changed device name from \"" + orig_name + "\" to \"" + new_name + "\"") 546. return True 547. 548. """ 549. Function: Recv_handler 550. receives all packets from ZigBee modules (runs on separate thread) 551. handles packets containing change detection samples 552. """ 553. def Recv_handler(self, packet): 554. 555. #self.Log("receved packet:") 556. #self.Log(str(packet)) 557. 558. # acquire process packets lock 559. acquired = self._process_packets_lock.acquire(blocking=False) 560. 561. # if could not get lock (other thread is receiving packets) 562. if(not acquired): 563. # put packet into queue 564. self._packet_queue.put(packet, block=True, timeout=DEFAULT_TIMEOUT) 565. return 566. 567. # if could get lock: 568. 569. try: 570. # if discovery packet response 571. if("parameter" in packet): 572. discovery_data = packet['parameter'] 573. if("node_identifier" in discovery_data): 574. with self._db_lock: 575. 576. self.Log("received discovery packet response") 577. 578. # get mac address 579. device_mac = self.Bytes2mac(bytearray(discovery_data['source_addr_long'])) 580. 581. # check if already in db 582. if(self.Mac2name(device_mac)): 583. self.Log("discovered device that is already in the db") 584. return 585. 586. # try to identify device using node identifier 587. node_identifier = discovery_data["node_identifier"].decode("utf-8") 588. 589. self.Log("NI = " + node_identifier) 590.

591. split_ident = node_identifier.split("-") 592. 593. if(len(split_ident) == 2): 594. # get needed values 595. device_type = split_ident[0] 596. else: 597. self.Log("can't add discovered device, unrecognized identifier: " + str(node_identifier)) 598. return 599. 600. # attempt to add to db 601. success = self.Add_device(node_identifier, device_mac, device_type) 602. 603. if(success): 604. self.Log("discovered device with mac \"" + device_mac + "\" of type \"" + device_type + "\"") 605. self.Log("device named \"" + node_identifier + "\", use change_name command to change it to a better name") 606. return 607. else: 608. self.Log("failed to add discovered device to db") 609. return 610. finally: 611. # release process_packets lock 612. self._process_packets_lock.release() 613. 614. """ 615. Function: Discover_devices 616. sends network discovery command to local zigbee. 617. discovered devices are handled in Recv_handler 618. """ 619. def Send_discovery_packet(self): 620. self.Log("sending device discovery packet") 621. 622. # get lock 623. with self._zb_lock: 624. # tell local zigbee to discover devices on network 625. self._zb.at(command='ND') 626. 627. """ 628. Function: Add_task 629. given a dict of commands, adds a task to apscheduler 630. 631. required params: 632. task_type : "repeating", "once" 633. task_id : unique identifying string 634. task_command : any valid set device level command 635. 636. repeating: 637. one or more of the following: "year", "month", "day", "hour", "minute", "second" 638. 639. once: 640. all of the following: "year", "month", "day", "hour", "minute", "second" 641. """ 642. def Add_task(self, params): 643. 644. if("task_type" not in params): 645. self.Log("can't add task, no \"schedule_type\" in parameters") 646. return False 647. 648. if("task_id" not in params): 649. self.Log("can't add task, no \"task_id\" in parameters") 650. return False 651. 652. if("task_command" not in params): 653. self.Log("can't add task, no \"task_command\" in parameters") 654. return False 655. 656. task_command = params["task_command"]

657. task_type = params['task_type'] 658. task_id = params["task_id"] 659. 660. # create copy of params to pass to run_command function 661. run_params = params 662. 663. # re define command to task command 664. run_params["command"] = task_command 665. 666. # if interval type 667. if(task_type == "interval"): 668. if("year" in params): 669. year = int(params["year"]) 670. if("month" in params): 671. month = params["month"] 672. if("day" in params): 673. day = params["day"] 674. if("hour" in params): 675. hour = int(params["hour"]) 676. if("minute" in params): 677. minute = int(params["minute"]) 678. if("second" in params): 679. second = int(params["second"]) 680. 681. # add job 682. self._sched.add_job(self._task_function, trigger='interval', years=year, months=month, days=day, hours=hour, minutes=minute, seconds=second, args=[run_params], id=task_id, replace_existing=True) 683. 684. # if repeating type 685. elif(task_type == "cron"): 686. 687. year = None 688. month = None 689. day = None 690. hour = None 691. minute = None 692. second = None 693. 694. if("year" in params): 695. year = int(params["year"]) 696. if("month" in params): 697. month = params["month"] 698. if("day" in params): 699. day = params["day"] 700. if("hour" in params): 701. hour = int(params["hour"]) 702. if("minute" in params): 703. minute = int(params["minute"]) 704. if("second" in params): 705. second = int(params["second"]) 706. 707. # add job 708. self._sched.add_job(self._task_function, trigger='cron', year=year, month=month, day=day, hour=hour, minute=minute, second=second, args=[run_params], id=task_id, replace_existing=True) 709. 710. # if single occurance type 711. elif(task_type == "once"): 712. 713. if(not("year" in params and "month" in params and "day" in params and "hour" in params 714. and "minute" in params and "seconds" in params)): 715. self.Log("adding task failed, did not include one of these required params: year, month, day, hour, minute, second") 716. return False 717. 718. year = int(params["year"]) 719. month = int(params["month"]) 720. day = int(params["day"])

721. hour = int(params["hour"]) 722. minute = int(params["minute"]) 723. second = int(params["second"]) 724. 725. self._sched.add_job(self._task_function, trigger='date', 726. run_date=time.datetime(year, month, day, hour, minute, second), 727. args=[task_command], id=task_id, replace_existing=True) 728. 729. else: 730. self.Log("invalid task type \"" + task_type + "\"") 731. return False 732. 733. self.Log("added task \"" + task_id + "\" to schedule") 734. return True 735. 736. def Remove_task(self, task_id): 737. self._sched.remove_job(task_id) 738. self.Log("removed task \"" + task_id + "\" from schedule") 739. 740. def Get_tasks(self): 741. return self._sched.get_jobs() 742. 743. """ 744. Function: Run_command 745. recieves a dict of command to execute 746. commands = test, get(level), set(level), add(name, mac, type), remove(name) 747. """ 748. def Run_command(self, params): 749. 750. self.Log("running command: " + str(params)) 751. 752. if("task_id" in params): 753. self.Log("executing task \"" + params["task_id"] + "\"") 754. 755. # get the command 756. if("cmd" in params): 757. command = params["cmd"] 758. elif("command" in params): 759. command = params["commands"] 760. else: 761. command = "invalid" 762. 763. # test 764. if (command == "test"): 765. self.Log("receieved test command") 766. return("test:ok") 767. 768. # set level 769. elif (command == "set_device_level"): 770. 771. if('name' not in params): 772. self.Log("cannot run set command, must specify \"name\"") 773. return(command + ":failed") 774. 775. # get device name 776. device_name = params['name'] 777. 778. if('level' not in params): 779. self.Log("cannot run set command, must specify \"level\"") 780. return(command + ":failed") 781. 782. # get wanted device level 783. level = int(params['level']) 784. 785. if(level < 0 or level > 100): 786. self.Log("level was invalid")

787. return(command + ":failed") 788. 789. success = self.Set_device_level(device_name, int(level)) 790. 791. if(not success): 792. return(command + ":failed") 793. else: 794. return(command + ":ok") 795. 796. # get level 797. elif(command == "get_device_level"): 798. 799. if('name' not in params): 800. self.Log("cannot run get command, must specify \"name\"") 801. return(command + ":failed") 802. 803. # get device name 804. device_name = params['name'] 805. 806. curr_level = self.Sample_device(device_name) 807. 808. if(curr_level == LEVEL_UNK): 809. return(command + ":unk") 810. else: 811. return(command + ":" + str(curr_level)) 812. 813. # add a device 814. elif(command == "add_device"): 815. 816. if('name' not in params): 817. self.Log("cannot run add command, must specify \"name\"") 818. return("add:failed") 819. 820. if('mac' not in params): 821. self.Log("cannot run add command, must specify \"mac\"") 822. return("add:failed") 823. 824. if('type' not in params): 825. self.Log("cannot run add command, must specify \"type\"") 826. return("add:failed") 827. 828. # get device name, mac addr, and type 829. device_name = params['name'] 830. mac = params['mac'] 831. device_type = params['type'] 832. 833. success = self.Add_device(device_name, mac, device_type) 834. 835. if(success): 836. return(command + ":ok") 837. else: 838. return(command + ":failed") 839. 840. # remove a device 841. elif(command == "remove_device"): 842. 843. if('name' not in params): 844. self.Log("cannot run remove command, must specify \"name\"") 845. return("remove:failed") 846. 847. device_name = params['name'] 848. 849. success = self.Remove_device(device_name) 850. 851. if(success): 852. return(command + ":ok")

853. else: 854. return(command + ":failed") 855. 856. # change a device name 857. elif(command == "change_name"): 858. 859. if("name" not in params): 860. self.Log("cannot run change_name command, must specify \"name\"") 861. return("change_name:failed") 862. 863. if("new_name" not in params): 864. self.Log("cannot run change_name command, must specify \"new_name\"") 865. return(command + ":failed") 866. 867. orig_name = params["name"] 868. new_name = params["new_name"] 869. 870. success = self.Change_device_name(orig_name, new_name) 871. 872. if(success): 873. return(command + ":ok") 874. else: 875. return(command + ":failed") 876. 877. # add a task 878. elif(command == "add_task"): 879. success = self.Add_task(params) 880. 881. if(success): 882. return(command + ":ok") 883. else: 884. return(command + ":failed") 885. 886. else: 887. self.Log("recieved invalid command") 888. return("invalid command") 889. 890. """ 891. Function: Log 892. prints string to console and log file with a timestamp 893. """ 894. def Log(self, s): 895. logstr = time.strftime(LOG_TIMESTAMP) + ": " + s + "\n" 896. logging.debug(logstr) 897. print(logstr, end="") 898. 899. def Run_task(task): 900. global myhome 901. myhome.Run_command(task) 902. 903. def Discover(): 904. global myhome 905. myhome.Send_discovery() 906. 907. myhome = Home(discover_function=Discover, task_function=Run_task) 908. 909. if(__name__ == "__main__"): 910. print("this is a library. import it to use it") 911. exit(0)

1. #!/usr/bin/env python3 2. 3. import sys 4. from flask import Flask, request 5. from flask_basicauth import BasicAuth 6. from home import myhome 7. 8. PORT = 5002 9. 10. def main(args): 11. # get instance of home server 12. global myhome 13. 14. # setup http request handler 15. app = Flask(__name__) 16. @app.route('/',methods=['GET', 'POST']) 17. def req_handler(): 18. # check if json format 19. if(request.is_json): 20. params = request.get_json() 21. else: 22. params = request.args 23. 24. print("received http request:\n" + str(params)) 25. 26. # execute command 27. return(myhome.Run_command(params)) 28. 29. # start http server 30. app.run(host='0.0.0.0', port=PORT) #, ssl_context='adhoc') 31. 32. if(__name__ == "__main__"): 33. main(sys.argv)

D. Touchscreen GUI Code

1. #!/usr/bin/env python 2. 3. import sys 4. import time 5. import requests 6. 7. from kivy.app import App 8. from kivy.uix.widget import Widget 9. from kivy.uix.gridlayout import GridLayout 10. from kivy.uix.floatlayout import FloatLayout 11. from kivy.uix.tabbedpanel import * 12. from kivy.uix.button import Button 13. from kivy.uix.togglebutton import ToggleButton 14. from kivy.uix.switch import Switch 15. from kivy.uix.textinput import TextInput 16. from kivy.uix.label import Label 17. from kivy.uix.popup import Popup 18. from kivy.lang import Builder 19. from kivy.clock import Clock 20. from kivy.properties import * 21. 22. SERVER_URL = "https://clayton039.localtunnel.me" 23. #SERVER_URL = "http://localhost:5000/" 24. TIME_FORMAT = "%A %m-%d %I:%M %p" 25. 26. class ThermTab(TabbedPanelItem): 27. def __init__(self,**kwargs): 28. super(ThermTab,self).__init__(**kwargs) 29. 30. self.text="Thermostat" 31. self.content = FloatLayout(background_normal="test.jpeg") 32. #background_normal = '', background_color=(1,0,0,1) 33. 34. #self.myclock = MyClock() 35. #self.content.add_widget(self.myclock) 36. 37. self.timelabel = Label(text=time.strftime(TIME_FORMAT), font_size=72, size_hint=(0.5, 0.2), pos_hint={'center_x': 0.5, 'center_y': 0.8}) 38. Clock.schedule_interval(self.update_timelabel, 3) 39. 40. self.content.add_widget(self.timelabel) 41. 42. self.curr_temp = 70 43. self.set_temp = 70 44. 45. self.curr_temp_label = Label(text="Current: 70 F", font_size=42, size_hint=(0.5, 0.1), pos_hint={'center_x': 0.45, 'center_y': 0.6}, text_size=(350, None)) 46. self.content.add_widget(self.curr_temp_label) 47. 48. self.set_temp_label = Label(text= "Set: 70 F", font_size=42, size_hint=(0.5, 0.1), pos_hint={'center_x': 0.45, 'center_y': 0.5}, text_size=(350, None)) 49. self.content.add_widget(self.set_temp_label) 50. 51. self.increase_temp_button = Button(text="+", font_size=48, size_hint=(0.1, 0.1), pos_hint={'center_x': 0.6, 'center_y': 0.55}, on_release=self.increase_temp) 52. self.content.add_widget(self.increase_temp_button) 53. self.decrease_temp_button = Button(text="-", font_size=48, size_hint=(0.1, 0.1), pos_hint={'center_x': 0.6, 'center_y': 0.45}, on_release=self.decrease_temp) 54. self.content.add_widget(self.decrease_temp_button) 55. 56. self.current_mode_label = Label(text="Mode: Heat", font_size=26, size_hint=(0.5, 0.1), pos_hint={'center_x': 0.45, 'center_y': 0.4}, text_size=(350, None)) 57. self.content.add_widget(self.current_mode_label)

58. 59. self.heat_mode_button = Button(text="Heat", size_hint=(0.1, 0.1), pos_hint={'center_x': 0.35, 'center_y': 0.2}, on_release=self.set_heat_mode) 60. self.content.add_widget(self.heat_mode_button) 61. self.cool_mode_button = Button(text="Cool", size_hint=(0.1, 0.1), pos_hint={'center_x': 0.45, 'center_y': 0.2}, on_release=self.set_cool_mode) 62. self.content.add_widget(self.cool_mode_button) 63. self.auto_mode_button = Button(text="Auto", size_hint=(0.1, 0.1), pos_hint={'center_x': 0.55, 'center_y': 0.2}, on_release=self.set_auto_mode) 64. self.content.add_widget(self.auto_mode_button) 65. self.off_mode_button = Button(text="Off", size_hint=(0.1, 0.1), pos_hint={'center_x': 0.65, 'center_y': 0.2}, on_release=self.set_off_mode) 66. self.content.add_widget(self.off_mode_button) 67. 68. Clock.schedule_interval(self.update_therm, 3) 69. 70. def update_timelabel(self, event): 71. self.timelabel.text = time.strftime(TIME_FORMAT) 72. 73. def update_therm(self, event): 74. pass 75. 76. def decrease_temp(self, event): 77. if (self.set_temp > 32): 78. self.set_temp -= 1 79. self.set_temp_label.text = "Set: " + str(self.set_temp) + " F" 80. 81. def increase_temp(self, event): 82. if (self.set_temp < 100): 83. self.set_temp += 1 84. self.set_temp_label.text = "Set: " + str(self.set_temp) + " F" 85. 86. def set_heat_mode(self, event): 87. self.current_mode_label.text = "Mode: Heat" 88. 89. def set_cool_mode(self, event): 90. self.current_mode_label.text = "Mode: Cool" 91. 92. def set_auto_mode(self, event): 93. self.current_mode_label.text = "Mode: Auto" 94. 95. def set_off_mode(self, event): 96. self.current_mode_label.text = "Mode: Off" 97. 98. class DeviceTab(TabbedPanelItem): 99. def __init__(self,**kwargs): 100. super(DeviceTab,self).__init__(**kwargs) 101. 102. self.text="Devices" 103. self.content = FloatLayout() 104. self.gridlayout = GridLayout(cols=3, rows=5) 105. self.content.add_widget(self.gridlayout) 106. self.add_button = Button(text="+", font_size=48, background_normal="", background_color=(0,0,1,.7), on_press=self.add_device, size_hint=(0.1, 0.2), pos_hint={'x': 0.9, 'y': 0}) 107. self.content.add_widget(self.add_button) 108. 109. def add_device(self, event): 110. #self.content.remove_widget(self.add_button) 111. self.gridlayout.add_widget(DeviceTile()) 112. #self.content.add_widget(self.add_button) 113. 114. class DeviceTile(FloatLayout): 115. def __init__(self,**kwargs): 116. super(DeviceTile,self).__init__(**kwargs) 117.

118. self.setup_window = DeviceSetupWindow(self, size_hint=(0.5 , 0.5), pos_hit={'x_center': 0.5, 'y_center': 0.5}, on_dismiss=self.setup_tile, title="Device Setup", auto_dismiss=False) 119. self.is_setup = BooleanProperty(False) 120. self.device_name = StringProperty("null") 121. self.device_id = StringProperty("null") 122. self.device_type = StringProperty("null") 123. 124. self.setup_window.open() 125. #self.add_widget(self.setup_window) 126. 127. def setup_tile(self, event): 128. if(not self.is_setup): 129. self.parent.remove_widget(self) 130. return 131. 132. self.label = Label(text=self.device_name, font_size=36, size_hint=(0.5, 0.5), pos_hint={'center_x': 0.5, 'center_y': 0.65}) 133. self.add_widget(self.label) 134. 135. self.switch = Switch(on_press=self.toggle, active=False, size_hint=(0.5, 0.5), pos_hint={'center_x': 0.5, 'center_y': 0.5}) 136. self.add_widget(self.switch) 137. 138. self.close_button = Button(background_normal = '', background_color=(1,0,0,1), text="x", font_size=26, pos_hint={'x': 0.9, 'y': 0.9}, size_hint=(.1, .1), on_press=self.close_tile) 139. self.add_widget(self.close_button) 140. 141. payload = {'cmd':'add', 'id': self.device_id, 'name': self.device_name, 'type': self.device_type} 142. r = requests.get(SERVER_URL, params=payload) 143. 144. payload = {'cmd':'get', 'name': self.device_name} 145. r = requests.get(SERVER_URL, params=payload) 146. if (r.text == "on"): 147. self.current_state = "on" 148. self.switch.active = True 149. else: 150. self.current_state = "off" 151. self.switch.active = False 152. 153. # schedule status update 154. Clock.schedule_interval(self.update_status, 3) 155. 156. def update_status(self, event): 157. payload = {'cmd':'get', 'name': self.device_name} 158. r = requests.get(SERVER_URL, params=payload) 159. if (r.text == "on"): 160. self.current_state = "on" 161. self.switch.active = True 162. else: 163. self.current_state = "off" 164. self.switch.active = False 165. 166. def toggle(self, event): 167. if(self.current_state == "off"): 168. self.current_state == "on" 169. else: 170. self.current_state = "off" 171. 172. payload = {'cmd':'set', 'name': self.device_name, 'to': self.current_state} 173. r = requests.get(SERVER_URL, params=payload) 174. 175. def close_tile(self, event): 176. # send remove command to server 177. payload = {'cmd':'remove', 'name':self.device_name} 178. r = requests.get(SERVER_URL, params=payload) 179. 180. # stop status updater 181. Clock.unschedule(self.update_status)

182. 183. # delete tile widget 184. self.parent.remove_widget(self) 185. 186. class DeviceSetupWindow(Popup): 187. def __init__(self,caller,**kwargs): 188. super(DeviceSetupWindow,self).__init__(**kwargs) 189. 190. self.caller = caller 191. self.content = FloatLayout() 192. 193. # add close button 194. self.close_button = Button(text="x", background_normal = '', background_color=(1,0,0,1), pos_hint={'x': 0.9, 'y': 0.9}, size_hint=(0.1, 0.1), on_press=self.close_setupwindow) 195. 196. self.content.add_widget(self.close_button) 197. 198. # add buttons for choosing device type 199. self.outlet_button = ToggleButton(text="Outlet", group="type", on_press=self.toggle_outlet_button, pos_hint={'x': 0, 'y': .75}, size_hint=(.3, .25)) 200. self.outlet_active = False 201. self.content.add_widget(self.outlet_button) 202. self.lightswitch_button = ToggleButton(text="Lightswitch", group="type", on_press=self.toggle_lightswitch_button, pos_hint={'x': .3, 'y': .75}, size_hint=(0.3, 0.25)) 203. self.lightswitch_active = False 204. self.content.add_widget(self.lightswitch_button) 205. 206. # add entry boxes for device ID and Name 207. self.id_entry = TextInput(hint_text="Device ID", pos_hint={'x': 0, 'y': .5}, size_hint=(.6, .15), ) 208. self.content.add_widget(self.id_entry) 209. self.name_entry = TextInput(hint_text="Name", pos_hint={'x': 0, 'y': .3}, size_hint=(.6, .15)) 210. self.content.add_widget(self.name_entry) 211. 212. # add save button 213. self.save_button = Button(text="Save", background_normal="", background_color=(0,0,.7,1), on_press=self.save_setup, pos_hint={'x': 0, 'y': 0}, size_hint=(.4, .2)) 214. self.content.add_widget(self.save_button) 215. 216. def close_setupwindow(self, event): 217. self.caller.is_setup = False 218. self.dismiss() 219. 220. def toggle_outlet_button(self, event): 221. self.outlet_active = not self.outlet_active 222. 223. def toggle_lightswitch_button(self, event): 224. self.lightswitch_active = not self.lightswitch_active 225. 226. def save_setup(self, event): 227. if(self.outlet_active): 228. self.caller.device_type = "outlet" 229. elif(self.lightswitch_active): 230. self.caller.device_type = "lightswitch" 231. else: 232. return 233. 234. self.caller.device_id = self.id_entry.text 235. self.caller.device_name = self.name_entry.text 236. self.caller.is_setup = True 237. 238. # close setup window 239. #self.parent.remove_widget(self) 240. self.dismiss() 241. 242. class MainWindow(TabbedPanel): 243. def __init__(self,**kwargs):

244. super(MainWindow,self).__init__(**kwargs) 245. 246. self.do_default_tab = False 247. 248. self.therm_tab = ThermTab() 249. self.default_tab = self.therm_tab 250. 251. self.add_widget(self.therm_tab) 252. 253. self.device_tab = DeviceTab() 254. self.add_widget(self.device_tab) 255. 256. class TestApp(App): 257. 258. title = "Control Panel" 259. 260. def build(self): 261. return MainWindow() 262. 263. def main(args): 264. """ 265. # test connection to server 266. payload = {'cmd':'test'} 267. 268. try: 269. r = requests.get(SERVER_URL, params=payload) 270. except requests.exceptions.ConnectionError: 271. print("ERROR: could not connect to server") 272. return 273. 274. if (r.text == "OK"): 275. TestApp().run() 276. else: 277. print("ERROR: could not connect to server") 278. return 279. """ 280. TestApp().run() 281. 282. if (__name__ == "__main__"): 283. main(sys.argv)