tmpcontrol

package module
v0.0.0-...-07674f0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 26, 2024 License: Apache-2.0 Imports: 25 Imported by: 0

README

tmpcontrol: a lot cheaper than a kegerator

tmpcontrol runs on a raspberry pi with a DS18B20 temperature sensor and can control a heater/fridge with a Kasa smart plug.

Features

  • JSON configuration-driven
  • Standalone mode or push config from the web
  • Free use of our server (as long as we can maintain it😀️), or host your own
  • Temperature configuration can be scheduled, for example, for a fermentation temperature schedule
  • If you have a heating element, you can configure the mash water to be preheated by the morning
  • If you would like to receive text message notifications, you can Venmo me a few bucks to pay Twilio

Usage

-kasa-path /home/pi/.local/bin/kasa
-config-server-root-url https://tmpcontrol.online
-local-config-path pi-config.json
-client-identifier johns-basement
-config-fetch-interval 60

Setup

  1. Fetch the code base

    git clone https://github.com/jroedel/tmpcontrol.git

    or download the binary directly to the raspberry pi

    wget ...

  2. (optional if you already have the binary) Cross-compile the binary for your Pi

    cd cmd/tmpcontrol
    GOOS=linux GOARCH=arm GOARM=7 go build -o ../../bin/tmpcontrol
    
  3. Arrange to send the binary to the Pi via sftp or by copying it to the SD card

  4. Test that the program runs tmpcontrol

  5. Create a bash script to run tmpcontrol. This allows you to do some extra setup. nano start-temperature-control.sh

    #!/bin/bash
    
    # check what pid(s) match this script's name. If there is just one (ours), then we'll launch the controls
    echo Our PID: $$
    pids=$(/usr/bin/pgrep -f "/bin/bash /home/pi/start-temperature-control.sh")
    echo All PIDs with this script name: $pids
    if [ "$pids" = "$$" ]; then
      export ADMIN_NOTIFY_KEY="xxxxxxxxxxxx"
      export ADMIN_NOTIFY_NUMBER="+112355505678"
      /home/pi/tmpcontrol -local-config-path pi-config.json -kasa-path /home/pi/.local/bin/kasa >> /home/pi/temperature-control.out 2>&1
    else
      echo 'There is an instance running already.'
    fi
    

    chmod +x start-temperature-control.sh

  6. Schedule the bash script to run every 5 minutes in case it dies. Run crontab -e and add the line

    */5 * * * * /home/pi/start-temperature-control.sh
    

    You may also want to add a line to reboot the Pi daily in case memory is leaking for whatever reason (note the sudo to edit root's crontab):

    sudo crontab -e
    # add the following line to crontab to reboot at 6 pm daily
    0 18 * * * /usr/sbin/reboot
    
  7. Let tmpcontrol start on its own and monitor the output:

    tail -f temperature-control.out

Configuration examples

Prep mash water for when you wake up in the morning
{
  "controllers": [
    {
      "name":"test-config",
      "thermometerPath": "../../temperature.txt",
      "controlType": "heat",
      "switchHosts": ["192.168.0.11"],
      "disableFreezeProtection": false,
      "temperatureSchedule": {
         "2024-07-01T06:00:00Z": 150,
         "2024-07-01T07:00:00Z": 156
      }
    }
  ]
}
Keep kegs ready to serve, 33°F
{
  "controllers": [
    {
      "name":"test-config",
      "thermometerPath": "../../temperature.txt",
      "controlType": "cool",
      "switchHosts": ["192.168.0.11"],
      "disableFreezeProtection": false,
      "temperatureSchedule": {
         "2024-07-01T00:00:00Z": 33
      }
    }
  ]
}
Fermentation with cold crash
{
  "controllers": [
    {
      "name":"test-config",
      "thermometerPath": "../../temperature.txt",
      "controlType": "cool",
      "switchHosts": ["192.168.0.11"],
      "disableFreezeProtection": false,
      "temperatureSchedule": {
         "2024-07-01T00:00:00Z": 68,
         "2024-07-09T00:00:00Z": 62,
         "2024-07-09T12:00:00Z": 56,
         "2024-07-10T00:00:00Z": 50,
         "2024-07-10T12:00:00Z": 44,
         "2024-07-11T00:00:00Z": 38
      }
    }
  ]
}

Pending work

  • Provide Celsius support
  • Allow for email notifications sent from server
  • Make some of the notification intervals configurable by command line

FAQ

How do I configure the temperature sensor and find its PATH?

Documentation

Index

Constants

View Source
const ThermometerDevicesRootPath = "/sys/bus/w1/devices/"

ThermometerDevicesRootPath where to look for DS18B20 devices

Variables

View Source
var AtLeastOneHostControlFailed = fmt.Errorf("at least one host control failed")
View Source
var ClientIdentifiersRegex = regexp.MustCompile(`^[-a-zA-Z0-9]{3,50}$`)

ClientIdentifiersRegex pattern to which clientIdentifiers and controllerNames should adhere

View Source
var PostUnmarshalableJson error
View Source
var TemperatureReadError = errors.New("there was a problem reading the current temperature")

Functions

func IndexCheck404Middleware

func IndexCheck404Middleware(next http.Handler) http.Handler

func IndexCheckForFormGetSubmit

func IndexCheckForFormGetSubmit(next http.Handler) http.Handler

func IsJSON

func IsJSON(str []byte) bool

func LogRequestMiddleware

func LogRequestMiddleware(next http.Handler) http.Handler

LogRequestMiddleware logs basic info of a HTTP request RemoteAddr: Network address that sent the request (IP:port) Proto: Protocol version Method: HTTP method URL: Request URL

func SecureHeadersMiddleware

func SecureHeadersMiddleware(next http.Handler) http.Handler

SecureHeadersMiddleware adds two basic security headers to each HTTP response X-XSS-Protection: 1; mode-block can help to prevent XSS attacks X-Frame-Options: deny can help to prevent clickjacking attacks

func ValidateAndParseBody

func ValidateAndParseBody(next http.Handler) http.Handler

func WithLogger

func WithLogger(l Logger, next http.Handler) http.Handler

Types

type ApiMessage

type ApiMessage struct {
	Status  ApiStatus `json:"status"`
	Message string    `json:"message"`
}

type ApiStatus

type ApiStatus int
const (
	NOK ApiStatus = iota + 1
	OK
)

func (ApiStatus) String

func (s ApiStatus) String() string

type ClientDb

type ClientDb interface {
	PersistTmpLog(tmplog TmpLog) error
	io.Closer
}

type ConfigGopher

type ConfigGopher struct {
	LocalConfigPath string
	//ServerRoot includes the protocol scheme, hostname and port. Trailing '/' is optional
	ServerRoot string
	//ClientId the client identifier to let the server know who we are
	ClientId            string
	ConfigFetchInterval time.Duration
	//if a Writer is defined, server notifications will be written additionally to this Writer
	NotifyOutput io.Writer
}

func (*ConfigGopher) FetchConfig

func (cg *ConfigGopher) FetchConfig() (ControllersConfig, ConfigSource, error)

func (*ConfigGopher) GetSourceKind

func (cg *ConfigGopher) GetSourceKind() (ConfigSource, bool)

func (*ConfigGopher) HasError

func (cg *ConfigGopher) HasError() error

func (*ConfigGopher) NotifyServer

func (cg *ConfigGopher) NotifyServer(message string, urgency ServerNotificationUrgency)

NotifyServer Send the server a message If the server can't be contacted, queue the message in a text file maybe we can restructure all the logging code to use structured messages (with error levels). Above a certain error level could be automatically reported

func (*ConfigGopher) SendConfig

func (cg *ConfigGopher) SendConfig(config ControllersConfig) error

type ConfigSource

type ConfigSource int
const (
	ConfigSourceLocalFile ConfigSource = iota + 1
	ConfigSourceServer
)

func (ConfigSource) String

func (c ConfigSource) String() string

type Control

type Control int
const (
	// ControlOn Turn something on
	ControlOn Control = iota + 1
	// ControlOff Turn something off
	ControlOff
)

func (Control) String

func (c Control) String() string

type ControlLooper

type ControlLooper struct {
	Cg                   *ConfigGopher
	HeatOrCoolController HeatOrCoolController
	TemperatureReader    TemperatureReader

	Logger Logger
	// contains filtered or unexported fields
}

func NewControlLooper

func NewControlLooper(cg *ConfigGopher, HeatOrCoolController HeatOrCoolController, logger Logger) *ControlLooper

func (*ControlLooper) StartControlLoop

func (cl *ControlLooper) StartControlLoop()

type Controller

type Controller struct {
	Name                    string                `json:"name"`
	ThermometerPath         string                `json:"thermometerPath"`
	ControlType             string                `json:"controlType"`
	SwitchHosts             []string              `json:"switchHosts"`
	TemperatureSchedule     map[time.Time]float32 `json:"temperatureSchedule"`
	DisableFreezeProtection bool                  `json:"disableFreezeProtection"`
}

func (*Controller) GetCurrentDesiredTemperature

func (controller *Controller) GetCurrentDesiredTemperature() (float32, bool)

type ControllersConfig

type ControllersConfig struct {
	Controllers []Controller `json:"controllers"`
}

type DS18B20Reader

type DS18B20Reader struct {
	// contains filtered or unexported fields
}

func NewDS18B20Reader

func NewDS18B20Reader(logger Logger) *DS18B20Reader

func (DS18B20Reader) EnumerateThermometerPaths

func (t DS18B20Reader) EnumerateThermometerPaths() []string

EnumerateThermometerPaths Assuming we're on a Raspberry Pi, check if we can find any DS18B20 devices running

func (DS18B20Reader) ReadTemperatureInF

func (t DS18B20Reader) ReadTemperatureInF(temperaturePath string) (float32, error)

type HeatOrCoolController

type HeatOrCoolController interface {
	ControlDevice(host string, action Control) error
}

type KasaHeatOrCoolController

type KasaHeatOrCoolController struct {
	// contains filtered or unexported fields
}

func NewKasaHeatOrCoolController

func NewKasaHeatOrCoolController(kasaPath string) *KasaHeatOrCoolController

func (*KasaHeatOrCoolController) ControlDevice

func (k *KasaHeatOrCoolController) ControlDevice(host string, action Control) error

type Logger

type Logger interface {
	Printf(string, ...interface{})
}

type Notification

type Notification struct {
	NotificationId      int       `json:"notificationId"`
	ReportedAt          time.Time `json:"reportedAt"`
	ClientId            string    `json:"clientId"`
	Message             string    `json:"message"`
	Severity            string    `json:"severity"`
	HasUserBeenNotified bool      `json:"hasUserBeenNotified"`
}

Notification /*

type PostResult

type PostResult struct {
	// contains filtered or unexported fields
}

type Server

type Server struct {
	Mux     *http.ServeMux
	Address string
	// contains filtered or unexported fields
}

func NewServer

func NewServer(dbFileName string, l Logger) (*Server, error)

func (*Server) GetConfigurationHandler

func (s *Server) GetConfigurationHandler(w http.ResponseWriter, r *http.Request)

func (*Server) IndexHandler

func (s *Server) IndexHandler(w http.ResponseWriter, r *http.Request)

func (*Server) ListenAndServe

func (s *Server) ListenAndServe()

func (*Server) PostConfigurationHandler

func (s *Server) PostConfigurationHandler(w http.ResponseWriter, r *http.Request)

func (*Server) PostHtmlConfigurationHandler

func (s *Server) PostHtmlConfigurationHandler(w http.ResponseWriter, r *http.Request, result PostResult)

func (*Server) PostJsonConfigurationHandler

func (s *Server) PostJsonConfigurationHandler(w http.ResponseWriter, r *http.Request, l Logger, result PostResult)

type ServerDb

type ServerDb interface {
	//Configs: the main purpose of this server, to receive and server client config
	CreateOrUpdateConfig(clientId string, config ControllersConfig) error
	GetConfig(clientId string) (ControllersConfig, bool, error)
	ListNotifications(clientId string) ([]Notification, error)
	PutNotification(clientId string, note Notification) error

	//Check-ins: meant to detect offline clients
	//ClientIdCheckIn(clientId string) error
	//GetLastClientIdCheckIn(clientId string) (time.Time, error)
	io.Closer
}

type ServerNotificationUrgency

type ServerNotificationUrgency int
const (
	InfoNotification ServerNotificationUrgency = iota + 1
	ProblemNotification
	SeriousNotification
)

func (ServerNotificationUrgency) MarshalJSON

func (s ServerNotificationUrgency) MarshalJSON() ([]byte, error)

func (ServerNotificationUrgency) String

func (s ServerNotificationUrgency) String() string

func (*ServerNotificationUrgency) UnmarshalJSON

func (s *ServerNotificationUrgency) UnmarshalJSON(b []byte) error

type SmsNotifier

type SmsNotifier struct{}

func (SmsNotifier) Write

func (SmsNotifier) Write(p []byte) (int, error)

notifyAdminViaSms send and pray, this function will receive no logging

type SqliteClientDb

type SqliteClientDb struct {
	// contains filtered or unexported fields
}

func NewSqliteDbFromFilename

func NewSqliteDbFromFilename(filename string, logger Logger) (SqliteClientDb, error)

func (SqliteClientDb) Close

func (dbo SqliteClientDb) Close() error

func (SqliteClientDb) FetchTmpLogsNotYetSentToServer

func (dbo SqliteClientDb) FetchTmpLogsNotYetSentToServer() ([]TmpLog, error)

func (SqliteClientDb) GetAverageRecentTemperature

func (dbo SqliteClientDb) GetAverageRecentTemperature(controllerName string, d time.Duration) (float32, error)

func (SqliteClientDb) MarkTmpLogsAsSentToServer

func (dbo SqliteClientDb) MarkTmpLogsAsSentToServer(ids []int) error

MarkTmpLogsAsSentToServer TODO implement a limit to how many can be marked complete at at time

func (SqliteClientDb) PersistTmpLog

func (dbo SqliteClientDb) PersistTmpLog(tmplog TmpLog) error

type SqliteServerDb

type SqliteServerDb struct {
	// contains filtered or unexported fields
}

func NewSqliteServerDbFromFilename

func NewSqliteServerDbFromFilename(filename string, logger Logger) (SqliteServerDb, error)

func (SqliteServerDb) Close

func (dbo SqliteServerDb) Close() error

func (SqliteServerDb) CreateOrUpdateConfig

func (dbo SqliteServerDb) CreateOrUpdateConfig(clientId string, config ControllersConfig) error

CreateOrUpdateConfig TODO test me

func (SqliteServerDb) GetConfig

func (dbo SqliteServerDb) GetConfig(clientId string) (ControllersConfig, bool, error)

func (SqliteServerDb) ListNotifications

func (dbo SqliteServerDb) ListNotifications(clientId string) ([]Notification, error)

func (SqliteServerDb) PutNotification

func (dbo SqliteServerDb) PutNotification(clientId string, note Notification) error

type TempType

type TempType int
const (
	FermentationTemp TempType = iota + 1
	RoomTemp
	FridgeTemp
)

func (TempType) String

func (t TempType) String() string

type TemperatureReader

type TemperatureReader interface {
	// ReadTemperatureInF Returns the temperature in Fahrenheit
	ReadTemperatureInF(connectionString string) (float32, error)
}

type TmpLog

type TmpLog struct {
	ControllerName        string
	Timestamp             time.Time
	TemperatureInF        float32
	DesiredTemperatureInF float32
	IsHeatingNotCooling   bool
	TurningOnNotOff       bool
	HostsPipeSeparated    string

	//these should be left blank unless we get this from the local dbo
	DbAutoId            int
	ExecutionIdentifier string
}

Directories

Path Synopsis
cmd

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL