Set new Base version

master
git 2021-03-22 21:12:58 +01:00
commit efef8a9682
8 changed files with 803 additions and 0 deletions

143
.gitignore vendored Normal file
View File

@ -0,0 +1,143 @@
# Created by .ignore support plugin (hsz.mobi)
### Python template
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
/src/config.ini
\.idea

9
Dockerfile-1 Normal file
View File

@ -0,0 +1,9 @@
FROM python:3-alpine
COPY . /code/
RUN pip install -r /code/requirements.txt
WORKDIR /code/src/
CMD ["python","scheduler.py"]

9
Dockerfile-2 Normal file
View File

@ -0,0 +1,9 @@
FROM python:3-alpine
COPY . /code/
RUN pip install -r /code/requirements.txt
WORKDIR /code/src/
CMD ["python","server.py"]

148
README.md Normal file
View File

@ -0,0 +1,148 @@
# PiSwitch
A Raspberry Pi Relay controller based on the Astral Library
## Physical setup
Use physical pin numbering
![Pi pinout](https://www.jameco.com/Jameco/workshop/circuitnotes/raspberry_pi_circuit_note_fig2.jpg)
### Switch Toggle Button:
3.3v: either pin 1 or pin 17
signal: pin 11
### Astral Toggle Button / Switch:
3.3v: either pin 1 or pin 17
signal: pin 15
### LED's
5v: eiter pin 2 or pin 4
signal: pin 12
ground : any ground pin
### Relay
5v: eiter pin 2 or pin 4
signal: pin 38
ground : any ground pin
## Installation Process
Clone the repository
````shell
git clone https://git.dennisvandermeulen.nl/dennis/PiSwitch.git
cd PiSwitch/
````
Setup virtual environment & install requirements (skip if you use Docker)
````shell
python3 -m venv venv
source venv/bin/activate
pip3 install -r requirements.txt
````
Setup config.ini
````shell
nano src/config.ini
````
## Config.ini
Set up the base url for your api
````ini
[setup]
baseurl = http(s)://www.example.com:port/
````
setup switch id
````ini
switchid = integer
````
Setup whether information must be pushed to the database
````ini
storedb = 0/1
````
Set the apikey for this PiSwitch
````ini
apikey = string
````
Set the key of the [PiSwitch reporting server](https://git.dennisvandermeulen.nl/dennis/PiSwitch_reporting) (Do not use if storedb=0)
````ini
srvapikey = string
````
Setup your location info
````ini
[location]
name = name
region = region
lat = float
lon = float
timezone = Europe/Amsterdam
````
##Docker
Use the included Docker-Compose files to build the scheduler and server container
````shell
docker-compose . -up -d
````
## Systemd
Use the following scripts to automatically start the scripts at boot for a true headless experience
### server.py
````ini
[Unit]
Description=Relay Switch Server
After=multi-user.target
[Service]
Type=simple
User=root
Group=root
WorkingDirectory=/path/to/PiSwitch/src
ExecStart=/path/to/venv/bin/python3 path/to/PiSwitch/src/server.py
[Install]
WantedBy=multi-user.target
````
save as relayserver.service
### scheduler.py
````ini
[Unit]
Description=Relay Switch listener
After=multi-user.target
Requires=relayserver.service
[Service]
Type=simple
User=root
Group=root
WorkingDirectory=/path/to/PiSwitch/src
ExecStart=/path/to/venv/bin/python3 path/to/PiSwitch/src/scheduler.py
[Install]
WantedBy=multi-user.target
````
save as scheduler.service
## Troubleshooting
Check LED color for basic status indications
White Solid = No Wi-Fi
Red Solid = Lights off
Green Solid = Lights on
Blue Flashing = Astral enabled
Off = No power / Led dead :)

37
docker-compose.yml Normal file
View File

@ -0,0 +1,37 @@
version: "3.7"
services:
piswitch-api:
build:
context: .
dockerfile: Dockerfile-2
ports:
- 5000:5000
container_name: piswitch-api
image: piswitch/api
restart: unless-stopped
tty: true
volumes:
- ./:/code/
working_dir: /code/src/
networks:
- piswitch-network
piswitch-scheduler:
build:
context: .
dockerfile: Dockerfile-1
container_name: piswitch-scheduler
image: piswitch/scheduler
restart: unless-stopped
tty: true
volumes:
- ./:/code/
working_dir: /code/src/
networks:
- piswitch-network
networks:
piswitch-network:
driver: bridge

27
requirements.txt Normal file
View File

@ -0,0 +1,27 @@
Adafruit-Blinka==5.4.0
adafruit-circuitpython-neopixel==6.0.0
adafruit-circuitpython-pypixelbuf==2.1.2
Adafruit-PlatformDetect==2.17.0
Adafruit-PureIO==1.1.5
APScheduler==3.6.3
astral==2.2
certifi==2020.6.20
chardet==3.0.4
click==7.1.2
configparser==5.0.0
Flask==1.1.2
Flask-Cors==3.0.9
idna==2.10
itsdangerous==1.1.0
Jinja2==2.11.2
MarkupSafe==1.1.1
pyftdi==0.51.2
pyserial==3.4
pytz==2020.1
pyusb==1.1.0
requests==2.24.0
rpi-ws281x==4.2.4
six==1.15.0
tzlocal==2.1
urllib3==1.25.10
Werkzeug==1.0.1

210
src/scheduler.py Normal file
View File

@ -0,0 +1,210 @@
# /usr/bin/env python3
import datetime
import RPi.GPIO as GPIO
import requests
import socket
import time
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.interval import IntervalTrigger
from apscheduler.triggers.cron import CronTrigger
import configparser
import pytz
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.setup(22, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
turnOnTime = 0
timeUpdated = False
scheduler = BackgroundScheduler()
timezone = pytz.timezone('Europe/Amsterdam')
def buttoncallback(channel):
time.sleep(0.15)
if GPIO.input(17) != GPIO.HIGH:
pass
else:
requests.post(baseUrl + '/raspiswitch' + str(switchId) + '/override', data={'apikey': switchApiKey})
def toggleastral(channel):
if GPIO.input(22) == GPIO.HIGH:
requests.post(baseUrl + '/raspiswitch' + str(switchId) + '/toggleastral',
data={'command': 1, 'apikey': switchApiKey})
else:
requests.post(baseUrl + '/raspiswitch' + str(switchId) + '/toggleastral',
data={'command': 0, 'apikey': switchApiKey})
def syncastral():
today = datetime.datetime.now()
response = requests.get(baseUrl + '/raspiswitch' + str(switchId) + '/gettime',
params={'year': today.year, 'month': today.month, 'day': today.day, 'apikey': switchApiKey})
global turnOnTime
turnOnTime = datetime.datetime.strptime(response.json()['response'], '%a, %d %b %Y %H:%M:%S %Z')
global timeUpdated
timeUpdated = True
def usingastral():
requests.get(baseUrl + '/raspiswitch' + str(switchId) + '/usingastral', params={'apikey': switchApiKey})
def isconnected():
try:
socket.create_connection(("www.google.com", 80))
if centralLoggingEnabled:
requests.post(baseUrl + '/raspapi/status',
data={'id': switchId, 'datetime': datetime.datetime.now(), 'key': 'conn_status', 'value': 1,
'apikey': serverApiKey})
except OSError:
if centralLoggingEnabled:
requests.post(baseUrl + '/raspapi/status',
data={'id': switchId, 'datetime': datetime.datetime.now(), 'key': 'conn_status', 'value': 0,
'apikey': serverApiKey})
else:
print('connectionstatus ' + str(0))
# Turn off Switch
def turnoff():
response = requests.post(baseUrl + '/raspiswitch' + str(switchId) + '/switchoff', params={'apikey': switchApiKey})
if centralLoggingEnabled:
requests.post(baseUrl + '/raspapi/cmd',
data={'id': switchId, 'datetime': datetime.datetime.now(),
'response': 'Turned ' + str(response.json()['currentstatus']), 'apikey': serverApiKey})
requests.post(baseUrl + '/raspapi/status',
data={'id': switchId, 'datetime': datetime.datetime.now(), 'key': 'switch_status',
'value': int(response.json()['currentstatus']), 'apikey': serverApiKey})
else:
print(str(response.json()['response'] + ' ' + response.json()['currentstatus']))
# Turn on Switch
def turnon():
response = requests.post(baseUrl + '/raspiswitch' + str(switchId) + '/switchon', data={'apikey': switchApiKey})
if centralLoggingEnabled:
requests.post(baseUrl + '/raspapi/cmd',
data={'id': switchId, 'datetime': datetime.datetime.now(),
'response': 'Turned ' + str(response.json()['currentstatus']), 'apikey': serverApiKey})
requests.post(baseUrl + '/raspapi/status',
data={'id': switchId, 'datetime': datetime.datetime.now(), 'key': 'switch_status',
'value': int(response.json()['currentstatus']), 'apikey': serverApiKey})
else:
print(str(response.json()['response'] + ' ' + response.json()['currentstatus']))
# Initial timer setup
def setuptimers():
response = requests.get(baseUrl + '/raspiswitch' + str(switchId) + '/usingastral', params={'apikey': switchApiKey})
if bool(response.json()['currentstatus']) is True:
global timeUpdated
if timeUpdated is True:
# Set up the scheduled time for the lights to be turned off
if not scheduler.get_job('turnoff'):
trigger = CronTrigger(hour=23, minute=55)
scheduler.add_job(turnoff, trigger, id='turnoff')
requests.post(baseUrl + '/raspapi/status',
data={'id': switchId, 'datetime': datetime.datetime.now(),
'key': 'off_time', 'value': scheduler.get_job('turnoff').next_run_time,
'apikey': serverApiKey})
# Set up the scheduled time for the lights to be turned on
global turnOnTime
scheduler.add_job(turnon, 'date', run_date=turnOnTime, id='turnon')
requests.post(baseUrl + '/raspapi/status',
data={'id': switchId, 'datetime': datetime.datetime.now(),
'key': 'on_time', 'value': turnOnTime, 'apikey': serverApiKey})
timeUpdated = False
# Update timers from dashboard changes
def updatetimers():
response = requests.post(baseUrl + '/raspapi/gettime',
params={'id': int(switchId), 'key': 'off_time', 'apikey': serverApiKey})
time_processed = timezone.localize(datetime.datetime.combine(datetime.datetime.now().date(),
datetime.datetime.strptime(response.json()['response'],
'%H:%M:%S').time()))
# Reschedule turn off time
if time_processed != scheduler.get_job('turnoff').next_run_time:
print('rescheduling turn off')
scheduler.reschedule_job(job_id='turnoff', trigger='date', run_date=time_processed)
response = requests.post(baseUrl + '/raspapi/gettime',
params={'id': int(switchId), 'key': 'on_time', 'apikey': serverApiKey})
time_processed = timezone.localize(datetime.datetime.combine(datetime.datetime.now().date(),
datetime.datetime.strptime(response.json()['response'],
'%H:%M:%S').time()))
# Reschedule turn on time
if scheduler.get_job('turnon'):
if time_processed != scheduler.get_job('turnon').next_run_time:
print('rescheduling turn on')
scheduler.reschedule_job(job_id='turnon', trigger='date', run_date=time_processed)
if __name__ == '__main__':
config = configparser.ConfigParser()
config.read('config.ini')
baseUrl = config['setup']['baseurl']
switchId = config['setup']['switchid']
switchApiKey = config['setup']['apikey']
serverApiKey = config['setup']['srvapikey']
centralLoggingEnabled = config['setup']['storedb']
# On startup
# Check if lights are on (To make sure the LED is correct)
requests.post(baseUrl + '/raspiswitch' + str(switchId) + '/lightstatus', data={'apikey': switchApiKey})
# Sync Astral time (To set up timers)
syncastral()
time.sleep(1)
# Check if the time will be used (To make sure server.py is correct)
toggleastral(22)
time.sleep(1)
# Check every 30 seconds if wifi connection is present
trigger = IntervalTrigger(seconds=30)
scheduler.add_job(isconnected, trigger)
# Check every 15 seconds if Astral is being used
trigger = IntervalTrigger(seconds=15)
scheduler.add_job(usingastral, trigger)
# Update light on data everyday at 4:00
trigger = CronTrigger(hour=4)
scheduler.add_job(syncastral, trigger)
# Overrideswitch button
GPIO.add_event_detect(17, GPIO.RISING, callback=buttoncallback, bouncetime=800)
# Astral Toggle button
GPIO.add_event_detect(22, GPIO.BOTH, callback=toggleastral, bouncetime=800)
# Turn the lights off when it is 23:55
trigger = CronTrigger(hour=23, minute=55)
scheduler.add_job(turnoff, trigger, id='turnoff')
# Check every 10 minutes if time variables have been updated
trigger = IntervalTrigger(minutes=10)
scheduler.add_job(setuptimers, trigger)
# Setup timer in case Astral will be activated later
setuptimers()
# Check if the timer values have been updated from the dashboard
trigger = IntervalTrigger(minutes=5)
scheduler.add_job(updatetimers, trigger)
scheduler.start()
requests.post(baseUrl + '/raspapi/status',
data={'id': switchId, 'datetime': datetime.datetime.now(),
'key': 'off_time', 'value': scheduler.get_job('turnoff').next_run_time})
while True:
time.sleep(60)

220
src/server.py Normal file
View File

@ -0,0 +1,220 @@
# /usr/bin/env python3
from flask import Flask, jsonify, request
from flask_cors import CORS
import astral
import astral.sun
import datetime
import time
import argparse
import neopixel
import board
import RPi.GPIO as GPIO
import requests
import configparser
triggerOnTime = 0
lightStatus = False
useAstral = True
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
app = Flask(__name__)
CORS(app)
def verifyapikey(requestapikey):
if requestapikey == apiKey:
return True
return False
# Update LED to reflect if trigger times by Astral are currently used
@app.route('/usingastral', methods=['GET'])
def usingastral():
if verifyapikey(request.values.get('apikey')):
global useAstral
if useAstral is True:
strip[0] = (0, 0, 255)
time.sleep(0.2)
if lightStatus is True:
strip[0] = (0, 255, 0)
else:
strip[0] = (255, 0, 0)
return jsonify(response='Astral is being used', currentstatus=useAstral), 200
else:
return jsonify(response='Astral is not being used', currentstatus=useAstral), 200
return jsonify(response='invalid api key'), 401
# Update LED to reflect if the relay is open or closed (Used at startup)
@app.route('/lightstatus', methods=['POST'])
def lightstatus():
if verifyapikey(request.values.get('apikey')):
if lightStatus is False:
strip[0] = (255, 0, 0)
elif lightStatus is True:
strip[0] = (0, 255, 0)
return jsonify(response='Startup routine completed'), 200
return jsonify(response='invalid api key'), 401
# Update LED to reflect if internet works
@app.route('/nointernet', methods=['POST'])
def nointernet():
if verifyapikey(request.values.get('apikey')):
strip[0] = (85, 85, 85)
return jsonify(response='Internet not working'), 200
return jsonify(response='invalid api key'), 401
# Override Switch for current status of Light
@app.route('/override', methods=['POST'])
def switchoverride():
if verifyapikey(request.values.get('apikey')):
global lightStatus
# Turn on the light
if lightStatus is False:
# Turn on outside lights
GPIO.setup(20, GPIO.OUT, initial=1)
# Set the LED to green to indicate the lights outside are on
strip[0] = (0, 255, 0)
lightStatus = True
# Turn off the light
else:
# Turn off outside lights
GPIO.setup(20, GPIO.IN)
# Set the LED to red to indicate the lights outside are off
strip[0] = (255, 0, 0)
lightStatus = False
if int(centralLoggingEnabled):
requests.post(baseUrl + '/raspapi/cmd',
data={'id': int(switchId), 'response': 'override button pressed, status: ' + str(lightStatus),
'apikey': srvApiKey})
requests.post(baseUrl + '/raspapi/status',
data={'id': int(switchId), 'key': 'switch_status', 'value': int(lightStatus),
'apikey': srvApiKey})
return jsonify(response='Toggled Lights', currentstatus=lightStatus), 200
return jsonify(response='invalid api key'), 401
# Toggle Switch for current status of Light
@app.route('/toggleastral', methods=['POST'])
def toggleastral():
if verifyapikey(request.values.get('apikey')):
global useAstral
useAstral = int(request.values.get('command'))
if int(centralLoggingEnabled):
requests.post(baseUrl + '/raspapi/cmd',
data={'id': int(switchId), 'response': 'toggled astral ' + str(useAstral), 'apikey': srvApiKey})
requests.post(baseUrl + '/raspapi/status',
data={'id': int(switchId), 'key': 'astral_status', 'value': int(useAstral), 'apikey': srvApiKey})
return jsonify(response='Toggled Astral', currentstatus=useAstral), 200
return jsonify(response='invalid api key'), 401
@app.route('/switchon', methods=['POST'])
def switchon():
if verifyapikey(request.values.get('apikey')):
# Turn on the light
GPIO.setup(20, GPIO.OUT, initial=1)
# Set the LED to green to indicate the lights are on
strip[0] = (0, 255, 0)
global lightStatus
lightStatus = True
if int(centralLoggingEnabled):
requests.post(baseUrl + '/raspapi/cmd',
data={'id': switchId, 'response': 'Turned switch' + str(lightStatus), 'apikey': srvApiKey})
requests.post(baseUrl + '/raspapi/status',
data={'id': int(switchId), 'key': 'switch_status', 'value': int(lightStatus),
'apikey': srvApiKey})
return jsonify(response='Switched the light on', currenstatus=lightStatus), 200
return jsonify(response='invalid api key'), 401
@app.route('/switchoff', methods=['POST'])
def switchoff():
if verifyapikey(request.values.get('apikey')):
# Turn off the light
GPIO.setup(20, GPIO.IN)
# Set the LED color to red to indicate the lights are off
strip[0] = (255, 0, 0)
global lightStatus
lightStatus = False
if int(centralLoggingEnabled):
requests.post(baseUrl + '/raspapi/cmd',
data={'id': int(switchId), 'response': 'Turned switch' + str(lightStatus), 'apikey': srvApiKey})
requests.post(baseUrl + '/raspapi/status',
data={'id': int(switchId), 'key': 'switch_status', 'value': int(lightStatus),
'apikey': srvApiKey})
return jsonify(response='Switched the light off', currentstatus=lightStatus), 200
return jsonify(response='invalid api key'), 401
@app.route('/updatelocation', methods=['POST'])
def updatelocation():
if verifyapikey(request.values.get('apikey')):
config = configparser.ConfigParser()
config.read('config.ini')
config['location']['name'] = request.values.get('loc_name')
config['location']['region'] = request.values.get('loc_region')
config['location']['lat'] = request.values.get('loc_lat')
config['location']['lon'] = request.values.get('loc_lon')
with open('config.ini', 'w') as configfile:
config.write(configfile)
if int(centralLoggingEnabled):
requests.post(baseUrl + '/raspapi/updatelocation',
data={'id': int(switchId),
'loc_name': request.values.get('loc_name'),
'loc_region': request.values.get('loc_region'),
'loc_lat': request.values.get('loc_lat'), 'loc_lon': request.values.get('loc_lon'),
'apikey': srvApiKey})
return jsonify(response='success'), 200
return jsonify(response='invalid api key'), 401
@app.route('/gettime', methods=['GET'])
def gettime():
if verifyapikey(request.values.get('apikey')):
config = configparser.ConfigParser()
config.read('config.ini')
name = config['location']['name']
region = config['location']['region']
lat = float(config['location']['lat'])
lon = float(config['location']['lon'])
timezone = config['location']['timezone']
# Get Sunset time for location
l = astral.LocationInfo(name=name, region=region, latitude=lat, longitude=lon, timezone=timezone)
global triggerOnTime
triggerOnTime = astral.sun.sunset(observer=l.observer,
date=datetime.date(int(request.args.get('year')),
int(request.args.get('month')),
int(request.args.get('day'))))
return jsonify(response=triggerOnTime), 200
return jsonify(response='invalid api key'), 401
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-c', '--clear', action='store_true', help='clear the display on exit')
args = parser.parse_args()
strip = neopixel.NeoPixel(board.D18, 1, brightness=0.2)
config = configparser.ConfigParser()
config.read('config.ini')
baseUrl = config['setup']['baseurl']
switchId = config['setup']['switchid']
apiKey = config['setup']['apikey']
srvApiKey = config['setup']['srvapikey']
centralLoggingEnabled = config['setup']['storedb']
app.run(host='0.0.0.0')