Compare commits
10 Commits
6c0054e72d
...
master
Author | SHA1 | Date | |
---|---|---|---|
83e838c9b7 | |||
741baa3c7a | |||
0d7b2a4935 | |||
cd8204e020 | |||
be6fb4bc3c | |||
ae30c39954 | |||
4a8d130c72 | |||
ad34a4b2d2 | |||
127c6ef91c | |||
84a643e69c |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -101,3 +101,5 @@ ENV/
|
||||
# Editor
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
out.txt
|
||||
|
@@ -1,6 +1,4 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ApiConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'api'
|
||||
name = 'apiserver.api'
|
||||
|
40
apiserver/apiserver/api/migrations/0001_initial.py
Normal file
40
apiserver/apiserver/api/migrations/0001_initial.py
Normal 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()),
|
||||
],
|
||||
),
|
||||
]
|
@@ -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,
|
||||
),
|
||||
]
|
@@ -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,
|
||||
),
|
||||
]
|
@@ -1,3 +1,28 @@
|
||||
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()
|
||||
|
@@ -1,7 +1,21 @@
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
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 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):
|
||||
@@ -9,3 +23,52 @@ class UserViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = serializers.UserSerializer
|
||||
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)
|
||||
|
||||
|
@@ -10,6 +10,10 @@ For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/4.0/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging.config
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
# 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!
|
||||
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 = True
|
||||
DEBUG_ENV = os.environ.get('DEBUG', False)
|
||||
DEBUG = DEBUG_ENV or False
|
||||
|
||||
ALLOWED_HOSTS = [
|
||||
'api.soak.stctech.ca',
|
||||
@@ -40,6 +44,8 @@ INSTALLED_APPS = [
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
#'rest_framework.authtoken',
|
||||
'apiserver.api',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
@@ -127,3 +133,43 @@ STATIC_URL = 'static/'
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
|
||||
|
||||
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')
|
||||
|
@@ -4,6 +4,7 @@ from apiserver.api import views
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.register(r'users', views.UserViewSet)
|
||||
router.register(r'data', views.DataViewSet, basename='data')
|
||||
|
||||
# Wire up our API using automatic URL routing.
|
||||
# Additionally, we include login URLs for the browsable API.
|
||||
|
41
rpiserver/bosminer.py
Normal file
41
rpiserver/bosminer.py
Normal 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
4
rpiserver/data.py
Normal file
@@ -0,0 +1,4 @@
|
||||
PROBES = {
|
||||
'0218404f8bff': 'Rad',
|
||||
'0218405068ff': 'Tub',
|
||||
}
|
117
rpiserver/main.py
Normal file
117
rpiserver/main.py
Normal 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
41
rpiserver/relays.py
Normal 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()
|
37
rpiserver/requirements.txt
Normal file
37
rpiserver/requirements.txt
Normal 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
22
rpiserver/w1therm.py
Normal 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()
|
||||
|
Reference in New Issue
Block a user