Compare commits

..

14 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
6c0054e72d Basic setup 2022-04-25 21:50:02 +00:00
7be3e6a39d Freeze requirements 2022-04-25 21:12:53 +00:00
ee5236e43a Unignore settings.py 2022-04-25 21:04:33 +00:00
8f62efd1ae DRF quickstart 2022-04-25 21:04:18 +00:00
25 changed files with 734 additions and 1 deletions

2
.gitignore vendored
View File

@@ -102,4 +102,4 @@ ENV/
*.swp *.swp
*.swo *.swo
settings.py out.txt

View File

View File

View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@@ -0,0 +1,4 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
name = 'apiserver.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

@@ -0,0 +1,28 @@
from django.db import models
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

@@ -0,0 +1,8 @@
from django.contrib.auth.models import User
from rest_framework import serializers
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ['url', 'username', 'email', 'groups']

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,74 @@
import logging
logger = logging.getLogger(__name__)
from django.contrib.auth.models import User
from rest_framework import viewsets, views, mixins
from rest_framework import permissions
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):
queryset = User.objects.all().order_by('-date_joined')
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)

View File

@@ -0,0 +1,16 @@
"""
ASGI config for apiserver project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'apiserver.settings')
application = get_asgi_application()

View File

@@ -0,0 +1,175 @@
"""
Django settings for apiserver project.
Generated by 'django-admin startproject' using Django 4.0.4.
For more information on this file, see
https://docs.djangoproject.com/en/4.0/topics/settings/
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'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
# 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'
DEBUG_ENV = os.environ.get('DEBUG', False)
DEBUG = DEBUG_ENV or False
ALLOWED_HOSTS = [
'api.soak.stctech.ca',
]
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
#'rest_framework.authtoken',
'apiserver.api',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'apiserver.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'apiserver.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'django',
'USER': 'django',
'PASSWORD': 'django',
'HOST': '127.0.0.1',
'PORT': '5432',
}
}
# Password validation
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# 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')

View File

@@ -0,0 +1,14 @@
from django.urls import include, path
from rest_framework import routers
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.
urlpatterns = [
path('', include(router.urls)),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]

View File

@@ -0,0 +1,16 @@
"""
WSGI config for apiserver project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'apiserver.settings')
application = get_wsgi_application()

22
apiserver/manage.py Executable file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'apiserver.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,6 @@
asgiref==3.5.0
Django==4.0.4
djangorestframework==3.13.1
psycopg2==2.9.3
pytz==2022.1
sqlparse==0.4.2

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()