Compare commits

..

10 Commits

Author SHA1 Message Date
83e838c9b7 Add models and API routes 2022-08-07 20:33:06 +00:00
741baa3c7a Improve logging 2022-08-07 20:33:06 +00:00
0d7b2a4935 Make script more reliable 2022-06-17 05:09:07 +01:00
cd8204e020 Pull get_temp out of main loop 2022-06-17 03:41:09 +01:00
be6fb4bc3c Write simple thermostat 2022-06-09 07:56:39 +01:00
ae30c39954 Add relay drivers 2022-05-01 01:49:53 +01:00
4a8d130c72 Move RPi server files into rpiserver/ 2022-04-26 17:04:40 -06:00
ad34a4b2d2 Add bosminer API module 2022-04-26 17:03:18 -06:00
127c6ef91c Add driver for temperature probes 2022-04-26 17:00:56 -06:00
84a643e69c Freeze requirements 2022-04-26 17:00:56 -06:00
15 changed files with 507 additions and 8 deletions

2
.gitignore vendored
View File

@@ -101,3 +101,5 @@ ENV/
# Editor # Editor
*.swp *.swp
*.swo *.swo
out.txt

View File

@@ -1,6 +1,4 @@
from django.apps import AppConfig from django.apps import AppConfig
class ApiConfig(AppConfig): class ApiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' name = 'apiserver.api'
name = 'api'

View File

@@ -0,0 +1,40 @@
# Generated by Django 4.0.4 on 2022-04-27 23:47
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='CoolerData',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('time', models.DateTimeField()),
('cooler_id', models.CharField(blank=True, max_length=36)),
],
),
migrations.CreateModel(
name='MinerData',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('time', models.DateTimeField()),
('miner_id', models.CharField(blank=True, max_length=36)),
('summary', models.JSONField()),
('fans', models.JSONField()),
('devdetails', models.JSONField()),
('version', models.JSONField()),
('devs', models.JSONField()),
('config', models.JSONField()),
('coin', models.JSONField()),
('pools', models.JSONField()),
('tunerstatus', models.JSONField()),
('temps', models.JSONField()),
],
),
]

View File

@@ -0,0 +1,43 @@
# Generated by Django 4.0.4 on 2022-06-23 21:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='coolerdata',
name='fan',
field=models.IntegerField(default=0),
preserve_default=False,
),
migrations.AddField(
model_name='coolerdata',
name='max_temp',
field=models.DecimalField(decimal_places=4, default=0, max_digits=7),
preserve_default=False,
),
migrations.AddField(
model_name='coolerdata',
name='pump',
field=models.IntegerField(default=0),
preserve_default=False,
),
migrations.AddField(
model_name='coolerdata',
name='rad_temp',
field=models.DecimalField(decimal_places=4, default=0, max_digits=7),
preserve_default=False,
),
migrations.AddField(
model_name='coolerdata',
name='tub_temp',
field=models.DecimalField(decimal_places=4, default=0, max_digits=7),
preserve_default=False,
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 4.0.4 on 2022-06-24 00:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0002_coolerdata_fan_coolerdata_max_temp_coolerdata_pump_and_more'),
]
operations = [
migrations.AddField(
model_name='minerdata',
name='json_version',
field=models.IntegerField(default=1),
preserve_default=False,
),
]

View File

@@ -1,3 +1,28 @@
from django.db import models from django.db import models
# Create your models here. class MinerData(models.Model):
time = models.DateTimeField()
miner_id = models.CharField(max_length=36, blank=True)
json_version = models.IntegerField()
summary = models.JSONField()
fans = models.JSONField()
devdetails = models.JSONField()
version = models.JSONField()
devs = models.JSONField()
config = models.JSONField()
coin = models.JSONField()
pools = models.JSONField()
tunerstatus = models.JSONField()
temps = models.JSONField()
class CoolerData(models.Model):
time = models.DateTimeField()
cooler_id = models.CharField(max_length=36, blank=True)
tub_temp = models.DecimalField(max_digits=7, decimal_places=4)
rad_temp = models.DecimalField(max_digits=7, decimal_places=4)
max_temp = models.DecimalField(max_digits=7, decimal_places=4)
fan = models.IntegerField()
pump = models.IntegerField()

View File

@@ -1,7 +1,21 @@
import logging
logger = logging.getLogger(__name__)
from django.contrib.auth.models import User from django.contrib.auth.models import User
from rest_framework import viewsets from rest_framework import viewsets, views, mixins
from rest_framework import permissions from rest_framework import permissions
from apiserver.api import serializers from rest_framework.decorators import action
from rest_framework.response import Response
from apiserver.api import serializers, models
from datetime import datetime, timezone
Base = viewsets.GenericViewSet
List = mixins.ListModelMixin
Retrieve = mixins.RetrieveModelMixin
Create = mixins.CreateModelMixin
Update = mixins.UpdateModelMixin
Destroy = mixins.DestroyModelMixin
class UserViewSet(viewsets.ModelViewSet): class UserViewSet(viewsets.ModelViewSet):
@@ -9,3 +23,52 @@ class UserViewSet(viewsets.ModelViewSet):
serializer_class = serializers.UserSerializer serializer_class = serializers.UserSerializer
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]
class DataViewSet(Base, List, Retrieve):
@action(detail=False, methods=['post'])
def push(self, request):
miner_data = request.data['miner_data']
cooler_data = request.data['cooler_data']
for miner_id, miner in miner_data.items():
time = miner['summary'][0]['STATUS'][0]['When']
models.MinerData.objects.update_or_create(
time=datetime.fromtimestamp(time, tz=timezone.utc),
miner_id=miner_id,
defaults=dict(
json_version=2,
summary=miner['summary'][0]['SUMMARY'][0],
fans=miner['fans'][0]['FANS'],
devdetails=miner['devdetails'][0]['DEVDETAILS'],
version=miner['version'][0]['VERSION'][0],
devs=miner['devs'][0]['DEVS'],
config=miner['config'][0]['CONFIG'][0],
coin=miner['coin'][0]['COIN'][0],
pools=miner['pools'][0]['POOLS'],
tunerstatus=miner['tunerstatus'][0]['TUNERSTATUS'][0],
temps=miner['temps'][0]['TEMPS'],
),
)
time = cooler_data['time']
cooler_id = cooler_data['cooler_id']
models.CoolerData.objects.update_or_create(
time=datetime.fromtimestamp(time, tz=timezone.utc),
cooler_id=cooler_id,
defaults=dict(
tub_temp=cooler_data['tub_temp'],
rad_temp=cooler_data['rad_temp'],
max_temp=cooler_data['max_temp'],
fan=cooler_data['fan'],
pump=cooler_data['pump'],
),
)
logging.info('Added {} miner data points from cooler {}.'.format(
len(miner_data),
cooler_id,
))
return Response(200)

View File

@@ -10,6 +10,10 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.0/ref/settings/ https://docs.djangoproject.com/en/4.0/ref/settings/
""" """
import os
import logging.config
logger = logging.getLogger(__name__)
from pathlib import Path from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
@@ -22,8 +26,8 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-au(y+z)$-iy#(obif&ilg*_pn0j_+0u=q*p7h(3c-ii-euncwx' SECRET_KEY = 'django-insecure-au(y+z)$-iy#(obif&ilg*_pn0j_+0u=q*p7h(3c-ii-euncwx'
# SECURITY WARNING: don't run with debug turned on in production! DEBUG_ENV = os.environ.get('DEBUG', False)
DEBUG = True DEBUG = DEBUG_ENV or False
ALLOWED_HOSTS = [ ALLOWED_HOSTS = [
'api.soak.stctech.ca', 'api.soak.stctech.ca',
@@ -40,6 +44,8 @@ INSTALLED_APPS = [
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'rest_framework', 'rest_framework',
#'rest_framework.authtoken',
'apiserver.api',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@@ -127,3 +133,43 @@ STATIC_URL = 'static/'
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'medium': {
'format': '[%(asctime)s] [%(process)d] [%(levelname)7s] %(message)s'
},
},
'filters': {
},
'handlers': {
'console': {
'level': 'DEBUG',
'filters': [],
'class': 'logging.StreamHandler',
'formatter': 'medium'
},
},
'loggers': {
'gunicorn': {
'handlers': ['console'],
'level': 'DEBUG' if DEBUG else 'INFO',
'propagate': False,
},
'': {
'handlers': ['console'],
'level': 'DEBUG',
'propagate': True,
},
},
'root': {
'level': 'DEBUG' if DEBUG else 'INFO',
'handlers': ['console'],
},
}
logging.config.dictConfig(LOGGING)
if DEBUG: logger.info('Debug mode ON')
logger.info('Test logging for each thread')

View File

@@ -4,6 +4,7 @@ from apiserver.api import views
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet) router.register(r'users', views.UserViewSet)
router.register(r'data', views.DataViewSet, basename='data')
# Wire up our API using automatic URL routing. # Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API. # Additionally, we include login URLs for the browsable API.

41
rpiserver/bosminer.py Normal file
View File

@@ -0,0 +1,41 @@
import asyncio
import json
async def bosminer_cmd(host, command, param=None):
reader, writer = await asyncio.open_connection(host, 4028)
payload = dict(command=command)
if param:
payload['parameter'] = param
message = json.dumps(payload)
writer.write(message.encode())
await writer.drain()
data = await reader.readuntil(separator=b'\00')
writer.close()
await writer.wait_closed()
return data[:-1].decode()
async def test():
import sys
if len(sys.argv) == 3:
cmd = sys.argv[1]
param = sys.argv[2]
if len(sys.argv) == 2:
cmd = sys.argv[1]
param = None
else:
cmd = 'temps'
param = None
res = await bosminer_cmd('192.168.69.4', cmd, param)
j = json.loads(res)
print(json.dumps(j, indent=4))
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(test())
loop.close()

4
rpiserver/data.py Normal file
View File

@@ -0,0 +1,4 @@
PROBES = {
'0218404f8bff': 'Rad',
'0218405068ff': 'Tub',
}

117
rpiserver/main.py Normal file
View File

@@ -0,0 +1,117 @@
import os
import logging
DEBUG = os.environ.get('DEBUG', False)
logging.basicConfig(
format='[%(asctime)s] %(levelname)s %(module)s/%(funcName)s - %(message)s',
level=logging.DEBUG if DEBUG else logging.INFO)
import asyncio
from statistics import mean
from signal import *
import time
import w1therm
import relays
import data
import requests
HI_SETPOINT = 40.0
LO_SETPOINT = HI_SETPOINT - 5.0
PUMP_RELAY = relays.RELAY1
FAN_RELAY = relays.RELAY3
def controller_message(message):
payload = dict(misc=message)
r = requests.post('https://tbot.tannercollin.com/message', data=payload, timeout=10)
if r.status_code == 200:
return True
else:
logging.exception('Unable to communicate with controller! Message: ' + message)
return False
def set_fan_on():
relays.set_relay(FAN_RELAY, relays.RELAY_ON)
def set_fan_off():
relays.set_relay(FAN_RELAY, relays.RELAY_OFF)
def set_pump_on():
relays.set_relay(PUMP_RELAY, relays.RELAY_ON)
async def get_temp():
temps = await w1therm.get_temperatures()
if len(temps) != len(data.PROBES):
raise
temperature_log = []
for id_, temp in temps.items():
temperature_log.append('{}: {} C'.format(data.PROBES[id_], temp))
logging.info('Temperature probe ' + ', '.join(temperature_log))
temperature_list = list(temps.values())
temperature_mean = mean(temperature_list)
return temperature_mean
async def run_thermostat():
while True:
try:
temperature_mean = await get_temp()
except:
logging.error('Problem reading temperature probes! Turning fan on and sleeping 60s.')
relays.set_relay(FAN_RELAY, relays.RELAY_ON)
await asyncio.sleep(60)
continue
if temperature_mean > HI_SETPOINT:
logging.info('Turning fan on')
set_fan_on()
elif temperature_mean < LO_SETPOINT:
logging.info('Turning fan off')
set_fan_off()
await asyncio.sleep(10)
def run_on_exit(*args):
logging.info('Cryptosoak exiting, turning pump and fan on.')
controller_message('Cryptosoak exiting.')
set_pump_on()
set_fan_on()
exit()
async def feed_watchdog():
while True:
with open('/dev/watchdog', 'w') as wdt:
wdt.write('1')
await asyncio.sleep(5)
def init():
relays.init_relays()
set_pump_on()
for sig in (SIGABRT, SIGILL, SIGINT, SIGSEGV, SIGTERM):
signal(sig, run_on_exit)
logging.info('Signals initialized')
async def main():
controller_message('Cryptosoak booting up...')
logging.info('Initialization complete')
await run_thermostat()
if __name__ == '__main__':
init()
if not DEBUG:
logging.info('Waiting 60 seconds to boot...')
time.sleep(60)
loop = asyncio.get_event_loop()
loop.run_until_complete(main()).add_done_callback(run_on_exit)
if not DEBUG:
loop.run_until_complete(feed_watchdog())
loop.close()

41
rpiserver/relays.py Normal file
View File

@@ -0,0 +1,41 @@
import asyncio
import RPi.GPIO as GPIO
RELAY1 = 4
RELAY2 = 22
RELAY3 = 6
RELAY4 = 26
RELAY_ON = True
RELAY_OFF = False
RELAYS = [RELAY1, RELAY2, RELAY3, RELAY4]
def set_relay(r, state):
GPIO.output(r, state)
def all_off():
for r in RELAYS:
set_relay(r, RELAY_OFF)
def init_relays():
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
for r in RELAYS:
GPIO.setup(r, GPIO.OUT)
all_off()
async def test():
for r in RELAYS:
set_relay(r, RELAY_ON)
await asyncio.sleep(0.5)
set_relay(r, RELAY_OFF)
if __name__ == '__main__':
init_relays()
loop = asyncio.get_event_loop()
loop.run_until_complete(test())
loop.close()

View File

@@ -0,0 +1,37 @@
aiofiles==0.8.0
aiohttp==3.8.1
aiosignal==1.2.0
appdirs==1.4.4
async-timeout==4.0.2
attrs==21.4.0
certifi==2020.6.20
chardet==4.0.0
charset-normalizer==2.0.12
click==8.1.2
colorzero==1.1
dbus-python==1.2.16
distlib==0.3.1
distro==1.5.0
distro-info==1.0
filelock==3.0.12
frozenlist==1.3.0
gpiozero==1.6.2
idna==2.10
importlib-metadata==1.6.0
more-itertools==4.2.0
multidict==6.0.2
PyGObject==3.38.0
python-apt==2.2.1
requests==2.25.1
RPi.GPIO==0.7.0
six==1.16.0
spidev==3.5
ssh-import-id==5.10
supervisor==4.2.2
ufw==0.36
unattended-upgrades==0.1
urllib3==1.26.5
virtualenv==20.4.0+ds
w1thermsensor==2.0.0
yarl==1.7.2
zipp==1.0.0

22
rpiserver/w1therm.py Normal file
View File

@@ -0,0 +1,22 @@
import asyncio
from w1thermsensor import AsyncW1ThermSensor, Unit
async def get_temperatures():
temps = {}
for sensor in AsyncW1ThermSensor.get_available_sensors():
temps[sensor.id] = await sensor.get_temperature()
return temps
async def test():
temps = await get_temperatures()
for id_, temp in temps.items():
print('sensor', id_, ':' , temp, 'C')
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(test())
loop.close()