This commit is contained in:
Nicolas Duhamel 2022-10-28 19:20:04 +02:00
parent 11b3622abe
commit 6837a07beb
6 changed files with 1171 additions and 1194 deletions

408
main.go
View File

@ -1,274 +1,270 @@
package main package main
import ( import (
"citadel/heater/pkg/device" "citadel/heater/mqtt"
"citadel/heater/mqtt" "citadel/heater/pkg/device"
mqttpaho "github.com/eclipse/paho.mqtt.golang" "context"
"context" "encoding/json"
"encoding/json" "flag"
"os" "os"
"os/signal" "os/signal"
"syscall" "strings"
"time" "syscall"
"strings" "time"
"github.com/rs/zerolog"
"flag" mqttpaho "github.com/eclipse/paho.mqtt.golang"
"github.com/rs/zerolog"
) )
type Config struct { type Config struct {
Host string Host string
Port string Port string
Username string Username string
Password string Password string
Clientid string Clientid string
} }
type App struct { type App struct {
mqtt_hub *mqtt.Hub mqtt_hub *mqtt.Hub
ctx context.Context ctx context.Context
DeviceManager *device.DeviceManager DeviceManager *device.DeviceManager
PubChan chan device.Message PubChan chan device.Message
} }
func NewApp(conf Config) *App { func NewApp(conf Config) *App {
hub := mqtt.NewHub(&mqtt.Config{ hub := mqtt.NewHub(&mqtt.Config{
Host: conf.Host, Host: conf.Host,
Port: conf.Port, Port: conf.Port,
Username: conf.Username, Username: conf.Username,
Password: conf.Password, Password: conf.Password,
ClientID: conf.Clientid, ClientID: conf.Clientid,
CleanSession: true, CleanSession: true,
AutoReconnect: true, AutoReconnect: true,
Retained: false, Retained: false,
KeepAlive: 15 * time.Second, KeepAlive: 15 * time.Second,
MsgChanDept: 100, MsgChanDept: 100,
}) })
pubchan := make(chan device.Message, 10) pubchan := make(chan device.Message, 10)
app := &App{ app := &App{
mqtt_hub: hub, mqtt_hub: hub,
PubChan: pubchan, PubChan: pubchan,
} }
app.DeviceManager = device.NewDeviceManager(pubchan, app.subscribe) app.DeviceManager = device.NewDeviceManager(pubchan, app.subscribe)
return app return app
} }
func (app *App) subscribe( func (app *App) subscribe(
topic string, topic string,
qos int, qos int,
callback func(context.Context, mqttpaho.Message), callback func(context.Context, mqttpaho.Message),
) context.CancelFunc { ) context.CancelFunc {
ctx, cancelFunc := context.WithCancel(app.ctx) ctx, cancelFunc := context.WithCancel(app.ctx)
log := zerolog.Ctx(ctx) log := zerolog.Ctx(ctx)
go func(ctx context.Context, sub *mqtt.Subscriber) { go func(ctx context.Context, sub *mqtt.Subscriber) {
for { for {
select { select {
case msg, ok := <-sub.OnMessage: case msg, ok := <-sub.OnMessage:
if !ok { if !ok {
return return
} }
callback(ctx, msg) callback(ctx, msg)
case err, ok := <-sub.OnError: case err, ok := <-sub.OnError:
if !ok { if !ok {
return return
} }
log.Error().Err(err) log.Error().Err(err)
case <-ctx.Done(): case <-ctx.Done():
return return
} }
} }
}(ctx, app.mqtt_hub.Subscribe(ctx, topic, qos)) }(ctx, app.mqtt_hub.Subscribe(ctx, topic, qos))
log.Info().Str("topic", topic).Msg("subscribe") log.Info().Str("topic", topic).Msg("subscribe")
return cancelFunc return cancelFunc
} }
func (app *App) runTicker() { func (app *App) runTicker() {
ticker := time.NewTicker(30 * time.Second) ticker := time.NewTicker(30 * time.Second)
logger := zerolog.Ctx(app.ctx).With().Str("action", "ticker").Logger() logger := zerolog.Ctx(app.ctx).With().Str("action", "ticker").Logger()
logger.Info().Msg("Start ticker") logger.Info().Msg("Start ticker")
ctx := logger.WithContext(app.ctx) ctx := logger.WithContext(app.ctx)
go func() { go func() {
for { for {
select { select {
case <- app.ctx.Done(): case <-app.ctx.Done():
return return
case t := <-ticker.C: case t := <-ticker.C:
logger.Info().Time("at", t).Msg("Tick") logger.Info().Time("at", t).Msg("Tick")
app.DeviceManager.CheckAll(ctx) app.DeviceManager.CheckAll(ctx)
} }
} }
}() }()
} }
func (app *App) onSettingsMessage(ctx context.Context, msg mqttpaho.Message) { func (app *App) onSettingsMessage(ctx context.Context, msg mqttpaho.Message) {
// callback for topic /{prefix}/{tvr_id}/settings change's // callback for topic /{prefix}/{tvr_id}/settings change's
device_name := strings.Split(msg.Topic(), "/")[1] device_name := strings.Split(msg.Topic(), "/")[1]
logger := zerolog.Ctx(ctx).With(). logger := zerolog.Ctx(ctx).With().
Str("action", "onSettingsMessage"). Str("action", "onSettingsMessage").
Str("device", device_name). Str("device", device_name).
Logger() Logger()
logger.Debug().Str("topic", msg.Topic()).Str("payload", string(msg.Payload())).Msg("") ctx = logger.WithContext(ctx)
ctx = logger.WithContext(ctx)
var device_settings device.DeviceSettings logger.Debug().
if err := json.Unmarshal(msg.Payload(), &device_settings); err != nil { Str("topic", msg.Topic()).
logger.Error().Err(err).Msg("Parsing payload") Str("payload", string(msg.Payload())).
} Msg("")
tvr := device.Device{ var device_settings device.DeviceSettings
Name: device_name, if err := json.Unmarshal(msg.Payload(), &device_settings); err != nil {
Settings: device_settings, logger.Error().Err(err).Msg("Parsing payload")
} }
if _, ok := app.DeviceManager.Get(device_name); !ok { tvr := device.Device{
err := app.DeviceManager.Add(tvr) Name: device_name,
if err != nil { Settings: device_settings,
logger.Error().Err(err).Msg("Unexpected") }
return
}
} else {
err := app.DeviceManager.SetSettings(device_name, device_settings)
if err != nil {
logger.Error().Err(err).Msg("Unexpected")
return
}
}
err := app.DeviceManager.Check(ctx, device_name) if _, ok := app.DeviceManager.Get(device_name); !ok {
if err != nil { if err := app.DeviceManager.Add(tvr); err != nil {
logger.Error().Err(err).Msg("During device `Check`") logger.Error().Err(err).Msg("unexpected, abord")
} return
}
} else {
if err := app.DeviceManager.
SetSettings(device_name, device_settings); err != nil {
logger.Error().Err(err).Msg("unexpected, abord")
return
}
}
if err := app.DeviceManager.Check(ctx, device_name); err != nil {
logger.Error().Err(err).Msg("During device `Check`")
}
} }
func (app *App) onSetStateMessage(ctx context.Context, msg mqttpaho.Message) { func (app *App) onSetStateMessage(ctx context.Context, msg mqttpaho.Message) {
// callback for topic /{prefix}/{tvr_id}/state/set change's // callback for topic /{prefix}/{tvr_id}/state/set change's
device_name := strings.Split(msg.Topic(), "/")[1] device_name := strings.Split(msg.Topic(), "/")[1]
logger := zerolog.Ctx(ctx).With(). logger := zerolog.Ctx(ctx).With().
Str("action", "onSetStateMessage"). Str("action", "onSetStateMessage").
Str("device", device_name). Str("device", device_name).
Logger() Logger()
ctx = logger.WithContext(ctx) ctx = logger.WithContext(ctx)
var state device.DeviceState
if err := json.Unmarshal(msg.Payload(), &state); err != nil {
logger.Error().Err(err).Msg("Error while parsing payload")
return
}
var state device.DeviceState logger.Debug().Interface("state", state).Msg("new state")
if err := json.Unmarshal(msg.Payload(), &state); err != nil {
logger.Error().Err(err).Msg("Error while parsing payload")
}
logger.Info().Interface("state", state).Msg("new state")
device, ok := app.DeviceManager.Get(device_name)
if !ok {
logger.Error().Msg("Device not found, abord")
return
}
if err := device.SetState(&logger, state, app.PubChan); err != nil {
logger.Error().Err(err).Msg("")
return
}
if device, ok := app.DeviceManager.Get(device_name); ok {
if err := device.SetState(&logger, state, app.PubChan); err != nil {
logger.Error().Err(err).Msg("unexpected")
return
}
} else {
logger.Error().Msg("Device not found, abord")
}
} }
func (app *App) Run() { func (app *App) Run() {
ctx, ctxCancel := context.WithCancel(context.Background()) ctx, ctxCancel := context.WithCancel(context.Background())
output := zerolog.ConsoleWriter{Out: os.Stderr} output := zerolog.ConsoleWriter{Out: os.Stderr}
log := zerolog.New(output). log := zerolog.New(output).
With(). With().
Logger() Logger()
ctx = log.WithContext(ctx) ctx = log.WithContext(ctx)
defer ctxCancel()
defer ctxCancel() if err := app.mqtt_hub.Connect(ctx); err != nil {
log.Fatal().Err(err)
}
if err := app.mqtt_hub.Connect(ctx); err != nil { app.ctx = ctx
log.Fatal().Err(err)
}
app.ctx = ctx app.subscribe("heater/+/settings", 2, app.onSettingsMessage)
app.subscribe("heater/+/state/set", 2, app.onSetStateMessage)
app.subscribe("heater/+/settings", 2, app.onSettingsMessage) go func(ctx context.Context, pub *mqtt.Publisher) {
app.subscribe("heater/+/state/set", 2, app.onSetStateMessage) log := zerolog.Ctx(ctx).With().
Str("action", "publisher").
Logger()
defer log.Error().Msg("publisher stoped")
for {
select {
go func(ctx context.Context, pub *mqtt.Publisher) { case msg, ok := <-app.PubChan:
log := zerolog.Ctx(ctx).With(). if !ok {
Str("action", "publisher"). log.Error().Msg("publish PubChan Not OK")
Logger() return
defer log.Error().Msg("publisher stoped") }
for { log.Debug().
select { Str("topic", msg.Topic).
Str("payload", string(msg.Payload)).
Msg("publish")
case msg, ok := <-app.PubChan: pub.Publish(msg.Topic, 1, msg.Payload, msg.Retain)
if !ok {
log.Error().Msg("publish PubChan Not OK")
return
}
log.Debug().
Str("topic", msg.Topic).
Str("payload", string(msg.Payload)).
Msg("publish")
pub.Publish(msg.Topic, 1 ,msg.Payload, msg.Retain) case err, ok := <-pub.OnError:
if !ok {
log.Error().Msg("publish OnError Not OK")
return
}
log.Error().Err(err).Msg("publish OnError")
case err, ok := <-pub.OnError: case <-ctx.Done():
if !ok { return
log.Error().Msg("publish OnError Not OK") }
return }
} }(ctx, app.mqtt_hub.Publisher(ctx))
log.Error().Err(err).Msg("publish OnError")
case <-ctx.Done(): app.runTicker()
return
}
}
}(ctx, app.mqtt_hub.Publisher(ctx))
app.runTicker() for {
select {
for { case <-ctx.Done():
select { return
case <-ctx.Done(): }
return }
}
}
} }
func main() { func main() {
host := flag.String("host", "localhost", "mqtt host") host := flag.String("host", "localhost", "mqtt host")
port := flag.String("port", "1883", "mqtt port") port := flag.String("port", "1883", "mqtt port")
username := flag.String("username", "", "mqtt username") username := flag.String("username", "", "mqtt username")
password := flag.String("password", "", "mqtt password") password := flag.String("password", "", "mqtt password")
clientid := flag.String("clientid", "goheater", "mqtt client id") clientid := flag.String("clientid", "goheater", "mqtt client id")
flag.Parse() flag.Parse()
config := Config{ config := Config{
Host: *host, Host: *host,
Port: *port, Port: *port,
Username: *username, Username: *username,
Password: *password, Password: *password,
Clientid: *clientid, Clientid: *clientid,
} }
app := NewApp(config) app := NewApp(config)
go app.Run() go app.Run()
quit := make(chan os.Signal, 1) quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit <-quit
} }

View File

@ -1,376 +1,364 @@
package device package device
import ( import (
"github.com/eclipse/paho.mqtt.golang" "context"
"github.com/ohler55/ojg/jp" "encoding/json"
"github.com/ohler55/ojg/oj" "fmt"
"fmt" "time"
"time"
"encoding/json" mqtt "github.com/eclipse/paho.mqtt.golang"
"context" "github.com/ohler55/ojg/jp"
"github.com/rs/zerolog" "github.com/ohler55/ojg/oj"
// "reflect" "github.com/rs/zerolog"
// "reflect"
) )
var timeNow = func() time.Time { var timeNow = func() time.Time {
return time.Now() return time.Now()
} }
type Error string type Error string
func (e Error) Error() string { return string(e) } func (e Error) Error() string { return string(e) }
type DeviceState struct { type DeviceState struct {
Mode string `json:"mode"` Mode string `json:"mode"`
Setpoint int `json:"setpoint"` Setpoint int `json:"setpoint"`
Time time.Time `json:"time"` Time time.Time `json:"time"`
Program_name string `json:"program_name"` Program_name string `json:"program_name"`
Until_time time.Time `json:"until_time"` Until_time time.Time `json:"until_time"`
} }
func (s *DeviceState) Equivalent(state DeviceState) bool { func (s *DeviceState) Equivalent(state DeviceState) bool {
if state.Mode != s.Mode { if state.Mode != s.Mode {
return false return false
} }
if !state.Time.Equal(s.Time) { if !state.Time.Equal(s.Time) {
return false return false
} }
switch state.Mode { switch state.Mode {
case "always": case "always":
if state.Setpoint != s.Setpoint { if state.Setpoint != s.Setpoint {
return false return false
} }
case "until_next": case "until_next":
if state.Setpoint != s.Setpoint { if state.Setpoint != s.Setpoint {
return false return false
} }
case "until_time": case "until_time":
if state.Setpoint != s.Setpoint { if state.Setpoint != s.Setpoint {
return false return false
} }
if !state.Until_time.Equal(s.Until_time) { if !state.Until_time.Equal(s.Until_time) {
return false return false
} }
case "program": case "program":
if state.Program_name != s.Program_name { if state.Program_name != s.Program_name {
return false return false
} }
} }
return true return true
} }
// Internal state // Internal state
type Device struct { type Device struct {
Name string Name string
Settings DeviceSettings Settings DeviceSettings
CurrentSetpoint int CurrentSetpoint int
State DeviceState State DeviceState
} }
func (d *Device) StateTopic() string { func (d *Device) StateTopic() string {
return fmt.Sprintf("heater/%s/state", d.Name) return fmt.Sprintf("heater/%s/state", d.Name)
} }
func (d Device) ListenTopic() (string, error) { func (d Device) ListenTopic() (string, error) {
return d.Settings.TVR.FormatTopicState(d.Name) return d.Settings.TVR.FormatTopicState(d.Name)
} }
func (d *Device) Program() (WeekProgram, error) { func (d *Device) Program() (WeekProgram, error) {
// return current device program if specified or default one // return current device program if specified or default one
prog_name := "default" prog_name := "default"
if d.State.Program_name != "" { if d.State.Program_name != "" {
prog_name = d.State.Program_name prog_name = d.State.Program_name
} }
program, ok := d.Settings.Programs[prog_name] program, ok := d.Settings.Programs[prog_name]
if !ok { if !ok {
return WeekProgram{}, Error(fmt.Sprintf("device %s don't have %s program", d.Name, prog_name)) return WeekProgram{}, Error(fmt.Sprintf("device %s don't have %s program", d.Name, prog_name))
} }
return program, nil return program, nil
} }
func (d *Device) ProgramName() string {
func (d *Device) ProgramName() (string) { prog_name := "default"
prog_name := "default" if d.State.Program_name != "" {
if d.State.Program_name != "" { prog_name = d.State.Program_name
prog_name = d.State.Program_name }
} return prog_name
return prog_name
} }
func (d *Device) publishState(pubchan chan Message) error { func (d *Device) publishState(pubchan chan Message) error {
payload, err := json.Marshal(d.State) payload, err := json.Marshal(d.State)
if err != nil { if err != nil {
return err return err
} }
pubchan <- Message { pubchan <- Message{
Topic: d.StateTopic(), Topic: d.StateTopic(),
Payload: payload, Payload: payload,
Retain: true, Retain: true,
} }
return nil return nil
} }
func (d *Device) SetSetpoint(value int, pubchan chan Message) error { func (d *Device) SetSetpoint(value int, pubchan chan Message) error {
topic, err := d.Settings.TVR.FormatTopic(d.Name) topic, err := d.Settings.TVR.FormatTopic(d.Name)
if err != nil { if err != nil {
return err return err
} }
payload, err := d.Settings.TVR.FormatPayload(value) payload, err := d.Settings.TVR.FormatPayload(value)
if err != nil { if err != nil {
return err return err
} }
pubchan <- Message{ pubchan <- Message{
Topic: topic, Topic: topic,
Payload: []byte(payload), Payload: []byte(payload),
Retain: false, Retain: false,
} }
return nil return nil
} }
func (d *Device) SetState(log *zerolog.Logger, state DeviceState, pubchan chan Message) error { func (d *Device) SetState(log *zerolog.Logger, state DeviceState, pubchan chan Message) error {
// If same state do nothing // If same state do nothing
// else use checksetpoint for changing state // else use checksetpoint for changing state
if d.State.Equivalent(state) { if d.State.Equivalent(state) {
log.Debug().Msg("same state no change") log.Debug().Msg("same state no change")
return nil return nil
} }
d.State = state d.State = state
// ignore change info, we already known // ignore change info, we already known
_, err := d.update(log, pubchan) _, err := d.update(log, pubchan)
if err != nil { if err != nil {
return err return err
} }
if err := d.publishState(pubchan); err != nil { if err := d.publishState(pubchan); err != nil {
return err return err
} }
return nil return nil
} }
func (d *Device) CheckSetpoint(log *zerolog.Logger, pubchan chan Message) error { func (d *Device) CheckSetpoint(log *zerolog.Logger, pubchan chan Message) error {
change, err := d.update(log, pubchan) change, err := d.update(log, pubchan)
if err != nil { if err != nil {
return err return err
} }
if change { if change {
if err := d.publishState(pubchan); err != nil { if err := d.publishState(pubchan); err != nil {
return err return err
} }
} }
return nil return nil
} }
func (d *Device) update(log *zerolog.Logger, pubchan chan Message) (bool, error) { func (d *Device) update(log *zerolog.Logger, pubchan chan Message) (bool, error) {
// Handle all the update setpoint logic // Handle all the update setpoint logic
// push in pubChan a Message if setpoint need to be update // push in pubChan a Message if setpoint need to be update
*log = log.With(). *log = log.With().
Str("device", d.Name). Str("device", d.Name).
Int("current_setpoint", d.CurrentSetpoint). Int("current_setpoint", d.CurrentSetpoint).
Str("State.Mode", d.State.Mode). Str("State.Mode", d.State.Mode).
Int("State.Setpoint", d.State.Setpoint). Int("State.Setpoint", d.State.Setpoint).
Str("State.Program_name", d.State.Program_name). Str("State.Program_name", d.State.Program_name).
Logger() Logger()
log.Info().Msg("Check if setpoint need an update") log.Debug().Msg("check if setpoint need an update")
switch d.State.Mode { switch d.State.Mode {
case "always": case "always":
return d.handle_always(log, pubchan) return d.handle_always(log, pubchan)
case "until_time": case "until_time":
return d.handle_until_time(log, pubchan) return d.handle_until_time(log, pubchan)
case "until_next": case "until_next":
return d.handle_until_next(log, pubchan) return d.handle_until_next(log, pubchan)
case "program": case "program":
return d.handle_program(log, pubchan) return d.handle_program(log, pubchan)
default: default:
log.Info().Msg("Use default mode") log.Info().Msg("Use default mode")
return d.handle_reset_state(log, pubchan) return d.handle_reset_state(log, pubchan)
} }
} }
func (d *Device) onMessage(ctx context.Context, msg mqtt.Message) { func (d *Device) onMessage(ctx context.Context, msg mqtt.Message) {
log := zerolog.Ctx(ctx).With(). log := zerolog.Ctx(ctx).With().
Str("action", "device state receive"). Str("action", "device state receive").
Str("device", d.Name). Str("device", d.Name).
Logger() Logger()
log.Debug(). log.Debug().
Str("topic", msg.Topic()). Str("topic", msg.Topic()).
Str("payload", string(msg.Payload())). Str("payload", string(msg.Payload())).
Msg("Message get") Msg("Message get")
obj, err := oj.ParseString(string(msg.Payload())) obj, err := oj.ParseString(string(msg.Payload()))
if err != nil { if err != nil {
log.Error().Err(err).Msg("during payload parse") log.Error().Err(err).Msg("during payload parse")
return return
} }
x, err := jp.ParseString(d.Settings.TVR.Setpoint_state_jp) x, err := jp.ParseString(d.Settings.TVR.Setpoint_state_jp)
if err != nil { if err != nil {
log.Error().Err(err).Msg("while parsing payload") log.Error().Err(err).Msg("while parsing payload")
return return
} }
r := x.First(obj) r := x.First(obj)
if v, ok := r.(int64); ok{ if v, ok := r.(int64); ok {
d.CurrentSetpoint = int(v) d.CurrentSetpoint = int(v)
} else { } else {
log.Error().Err(err).Interface("parsing payload", r).Msg("while parsing payload") log.Error().Err(err).Interface("parsing payload", r).Msg("while parsing payload")
} }
} }
func (d *Device) handle_reset_state(log *zerolog.Logger, pubchan chan Message) (bool, error) { func (d *Device) handle_reset_state(log *zerolog.Logger, pubchan chan Message) (bool, error) {
// called when need to fallback to program mode previous or default // called when need to fallback to program mode previous or default
program, err := d.Program() program, err := d.Program()
if err != nil { if err != nil {
return false, err return false, err
} }
current_setpoint := program.Current() current_setpoint := program.Current()
value, err := current_setpoint.Value(d.Settings.Presets) value, err := current_setpoint.Value(d.Settings.Presets)
if err != nil { if err != nil {
return false, err return false, err
} }
d.State = DeviceState{ d.State = DeviceState{
Setpoint: value, Setpoint: value,
Mode: "program", Mode: "program",
Program_name: d.ProgramName(), Program_name: d.ProgramName(),
Time: timeNow(), Time: timeNow(),
} }
if d.CurrentSetpoint != value { if d.CurrentSetpoint != value {
log.Info().Msg("publish setpoint update") log.Info().Msg("publish setpoint update")
err = d.SetSetpoint(value, pubchan) err = d.SetSetpoint(value, pubchan)
if err != nil { if err != nil {
return false, err return false, err
} }
} }
return true, nil return true, nil
} }
func (d *Device) handle_always(log *zerolog.Logger, pubchan chan Message) (bool, error) { func (d *Device) handle_always(log *zerolog.Logger, pubchan chan Message) (bool, error) {
// return true if change made // return true if change made
if d.State.Setpoint != d.CurrentSetpoint { if d.State.Setpoint != d.CurrentSetpoint {
if err:= d.SetSetpoint(d.State.Setpoint, pubchan); err != nil { if err := d.SetSetpoint(d.State.Setpoint, pubchan); err != nil {
return false, err return false, err
} }
return true, nil return true, nil
} }
log.Info().Msg("no setpoint update") log.Info().Msg("no setpoint update")
return false, nil return false, nil
} }
func (d *Device) handle_until_time(log *zerolog.Logger, pubchan chan Message) (bool, error) { func (d *Device) handle_until_time(log *zerolog.Logger, pubchan chan Message) (bool, error) {
*log = log.With().Time("until_time", d.State.Until_time).Logger() *log = log.With().Time("until_time", d.State.Until_time).Logger()
if d.State.Until_time.Before(timeNow()) { if d.State.Until_time.Before(timeNow()) {
log.Info().Msg("until_time passed, reset") log.Info().Msg("until_time passed, reset")
return d.handle_reset_state(log, pubchan) return d.handle_reset_state(log, pubchan)
} }
if d.State.Setpoint != d.CurrentSetpoint { if d.State.Setpoint != d.CurrentSetpoint {
log.Info().Msg("need setpoint update") log.Info().Msg("need setpoint update")
if err := d.SetSetpoint(d.State.Setpoint, pubchan); err != nil { if err := d.SetSetpoint(d.State.Setpoint, pubchan); err != nil {
return false, err return false, err
} }
return true, nil return true, nil
} }
log.Info().Msg("no setpoint update") log.Info().Msg("no setpoint update")
return false, nil return false, nil
} }
func (d *Device) handle_until_next(log *zerolog.Logger, pubchan chan Message) (bool, error) { func (d *Device) handle_until_next(log *zerolog.Logger, pubchan chan Message) (bool, error) {
*log = log.With().Time("until_next", d.State.Time).Logger() *log = log.With().Time("until_next", d.State.Time).Logger()
program, err := d.Program() program, err := d.Program()
if err != nil { if err != nil {
return false, err return false, err
} }
next, err := program.NextTime(d.State.Time) next, err := program.NextTime(d.State.Time)
if err!= nil { if err != nil {
return false, err return false, err
} }
if timeNow().After(next) { if timeNow().After(next) {
// force current program // force current program
// reset state // reset state
log.Info().Time("now", timeNow()).Time("next", next).Msg("until_next expired") log.Info().Time("now", timeNow()).Time("next", next).Msg("until_next expired")
return d.handle_reset_state(log, pubchan) return d.handle_reset_state(log, pubchan)
} }
if d.State.Setpoint != d.CurrentSetpoint { if d.State.Setpoint != d.CurrentSetpoint {
log.Info().Msg("need setpoint update") log.Info().Msg("need setpoint update")
if err := d.SetSetpoint(d.State.Setpoint, pubchan); err != nil { if err := d.SetSetpoint(d.State.Setpoint, pubchan); err != nil {
return false, err return false, err
} }
return true, nil return true, nil
} }
log.Info().Msg("no setpoint update") log.Info().Msg("no setpoint update")
return false, nil return false, nil
} }
func (d *Device) handle_program(log *zerolog.Logger, pubchan chan Message) (bool, error) { func (d *Device) handle_program(log *zerolog.Logger, pubchan chan Message) (bool, error) {
*log = log.With().Str("program", d.State.Program_name).Logger() *log = log.With().Str("program", d.State.Program_name).Logger()
program, err := d.Program() program, err := d.Program()
if err != nil { if err != nil {
return false, err return false, err
} }
current_setpoint := program.Current() current_setpoint := program.Current()
value, err := current_setpoint.Value(d.Settings.Presets) value, err := current_setpoint.Value(d.Settings.Presets)
if err != nil { if err != nil {
return false, err return false, err
} }
if d.CurrentSetpoint != value { if d.CurrentSetpoint != value {
log.Info().Msg("publish setpoint update") log.Info().Msg("publish setpoint update")
if err := d.SetSetpoint(value, pubchan); err != nil { if err := d.SetSetpoint(value, pubchan); err != nil {
return false, err return false, err
} }
d.State.Setpoint = value d.State.Setpoint = value
return true, nil return true, nil
} }
log.Info().Msg("no setpoint update") log.Info().Msg("no setpoint update")
return false, nil return false, nil
} }

View File

@ -1,99 +1,100 @@
package device package device
import ( import (
"fmt" "context"
"context" "fmt"
"github.com/eclipse/paho.mqtt.golang"
"github.com/rs/zerolog" mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/rs/zerolog"
) )
type Subscriber func(string, int, func(context.Context, mqtt.Message)) context.CancelFunc type Subscriber func(string, int, func(context.Context, mqtt.Message)) context.CancelFunc
type DeviceManager struct { type DeviceManager struct {
devices map[string]*Device devices map[string]*Device
subscriber Subscriber subscriber Subscriber
sub_cancel map[string]context.CancelFunc sub_cancel map[string]context.CancelFunc
PubChan chan Message PubChan chan Message
} }
func NewDeviceManager(pubchan chan Message, subscriber Subscriber) *DeviceManager { func NewDeviceManager(pubchan chan Message, subscriber Subscriber) *DeviceManager {
return &DeviceManager{ return &DeviceManager{
devices: make(map[string]*Device), devices: make(map[string]*Device),
sub_cancel: make(map[string]context.CancelFunc), sub_cancel: make(map[string]context.CancelFunc),
subscriber: subscriber, subscriber: subscriber,
PubChan: pubchan, PubChan: pubchan,
} }
} }
func (m *DeviceManager) Get(name string) (*Device, bool) { func (m *DeviceManager) Get(name string) (*Device, bool) {
device, ok := m.devices[name] device, ok := m.devices[name]
return device, ok return device, ok
} }
func (m *DeviceManager) Add(device Device) error { func (m *DeviceManager) Add(device Device) error {
if _, prs := m.devices[device.Name]; prs { if _, prs := m.devices[device.Name]; prs {
return fmt.Errorf("device %s already exist", device.Name) return fmt.Errorf("device %s already exist", device.Name)
} }
m.devices[device.Name] = &device m.devices[device.Name] = &device
// subscribe to state topic // subscribe to state topic
topic, err := device.ListenTopic() topic, err := device.ListenTopic()
if err != nil { if err != nil {
return err return err
} }
if topic != "" { if topic != "" {
cancel := m.subscriber(topic, 2, device.onMessage) cancel := m.subscriber(topic, 2, device.onMessage)
m.sub_cancel[device.Name] = cancel m.sub_cancel[device.Name] = cancel
} }
return nil return nil
} }
func (m *DeviceManager) Delete(name string) error { func (m *DeviceManager) Delete(name string) error {
return nil return nil
} }
func (m *DeviceManager) SetSettings(name string, settings DeviceSettings) error { func (m *DeviceManager) SetSettings(name string, settings DeviceSettings) error {
device, ok := m.devices[name] device, ok := m.devices[name]
if !ok { if !ok {
return fmt.Errorf("Not existings device %s", name) return fmt.Errorf("Not existings device %s", name)
} }
device.Settings = settings device.Settings = settings
return nil return nil
} }
func (m *DeviceManager) SetState(name string, state DeviceState) error { func (m *DeviceManager) SetState(name string, state DeviceState) error {
device, ok := m.devices[name] device, ok := m.devices[name]
if !ok { if !ok {
return fmt.Errorf("Not existings device %s", name) return fmt.Errorf("Not existings device %s", name)
} }
device.State = state device.State = state
return nil return nil
} }
func (m *DeviceManager) Check(ctx context.Context, name string) error { func (m *DeviceManager) Check(ctx context.Context, name string) error {
logger := zerolog.Ctx(ctx) logger := zerolog.Ctx(ctx)
device, ok := m.devices[name] device, ok := m.devices[name]
if !ok { if !ok {
return fmt.Errorf("Device %s don't exist", name) return fmt.Errorf("Device %s don't exist", name)
} }
err := device.CheckSetpoint(logger, m.PubChan) err := device.CheckSetpoint(logger, m.PubChan)
if err != nil { if err != nil {
logger.Error().Err(err).Msg("") logger.Error().Err(err).Msg("")
return err return err
} }
return nil return nil
} }
func (m *DeviceManager) CheckAll(ctx context.Context) error { func (m *DeviceManager) CheckAll(ctx context.Context) error {
logger := zerolog.Ctx(ctx) logger := zerolog.Ctx(ctx)
for _, device := range m.devices { for _, device := range m.devices {
err := device.CheckSetpoint(logger, m.PubChan) err := device.CheckSetpoint(logger, m.PubChan)
if err != nil { if err != nil {
logger.Error().Err(err).Msg("") logger.Error().Err(err).Msg("")
return err return err
} }
} }
return nil return nil
} }

View File

@ -1,459 +1,455 @@
package device package device
import ( import (
"testing" "fmt"
"time" "io/ioutil"
"fmt" "reflect"
"reflect" "testing"
"github.com/rs/zerolog" "time"
"io/ioutil"
"github.com/rs/zerolog"
) )
// monday // monday
var test_time = time.Date(2022, time.October, 24, 0, 0, 0, 0, time.Local) var test_time = time.Date(2022, time.October, 24, 0, 0, 0, 0, time.Local)
var test_presets = []Preset{ var test_presets = []Preset{
{Label: "default", Value: 17, Color: "#012a36"}, {Label: "default", Value: 17, Color: "#012a36"},
{Label: "normal", Value: 19, Color: "#b6244f"}, {Label: "normal", Value: 19, Color: "#b6244f"},
} }
var test_setpoints = []Setpoint{ var test_setpoints = []Setpoint{
{Start: 7*60, Preset_id: 1}, {Start: 7 * 60, Preset_id: 1},
{Start: 8*60, Preset_id: 0}, {Start: 8 * 60, Preset_id: 0},
{Start: 16*60, Preset_id: 1}, {Start: 16 * 60, Preset_id: 1},
{Start: 22*60, Preset_id: 0}, {Start: 22 * 60, Preset_id: 0},
} }
var test_weekprogram = WeekProgram{ var test_weekprogram = WeekProgram{
Monday: test_setpoints, Monday: test_setpoints,
Thuesday: test_setpoints, Thuesday: test_setpoints,
Wednesday: test_setpoints, Wednesday: test_setpoints,
Thursday : test_setpoints, Thursday: test_setpoints,
Friday: test_setpoints, Friday: test_setpoints,
Saturday: test_setpoints, Saturday: test_setpoints,
Sunday: test_setpoints, Sunday: test_setpoints,
} }
var test_programs = Programs{ var test_programs = Programs{
"default": test_weekprogram, "default": test_weekprogram,
} }
var test_device = Device { var test_device = Device{
Name: "valid", Name: "valid",
Settings: DeviceSettings{ Settings: DeviceSettings{
Programs: test_programs, Programs: test_programs,
Presets: test_presets, Presets: test_presets,
TVR: DefaultTVRSettings, TVR: DefaultTVRSettings,
}, },
CurrentSetpoint: 0, CurrentSetpoint: 0,
State: DeviceState{ State: DeviceState{
Mode: "program", Mode: "program",
Setpoint: 14, Setpoint: 14,
Time: test_time, Time: test_time,
Program_name: "default", Program_name: "default",
}, },
} }
func TestStateEquivalent(t *testing.T) { func TestStateEquivalent(t *testing.T) {
var tests = []struct{ var tests = []struct {
state1 DeviceState state1 DeviceState
state2 DeviceState state2 DeviceState
want bool want bool
}{ }{
{ {
DeviceState{ DeviceState{
Mode: "program", Mode: "program",
Setpoint: 14, Setpoint: 14,
Time: test_time, Time: test_time,
Program_name: "default", Program_name: "default",
}, },
DeviceState{ DeviceState{
Mode: "always", Mode: "always",
Setpoint: 13, Setpoint: 13,
Time: test_time, Time: test_time,
}, },
false, false,
}, },
{ {
DeviceState{ DeviceState{
Mode: "program", Mode: "program",
Setpoint: 14, Setpoint: 14,
Time: test_time, Time: test_time,
Program_name: "default", Program_name: "default",
}, },
DeviceState{ DeviceState{
Mode: "program", Mode: "program",
Setpoint: 14, Setpoint: 14,
Time: test_time.Add(1*time.Minute), Time: test_time.Add(1 * time.Minute),
Program_name: "default", Program_name: "default",
}, },
false, false,
}, },
{ {
DeviceState{ DeviceState{
Mode: "program", Mode: "program",
Setpoint: 14, Setpoint: 14,
Time: test_time, Time: test_time,
Program_name: "default", Program_name: "default",
}, },
DeviceState{ DeviceState{
Mode: "program", Mode: "program",
Setpoint: 13, Setpoint: 13,
Time: test_time, Time: test_time,
Program_name: "default", Program_name: "default",
}, },
true, true,
}, },
{ {
DeviceState{ DeviceState{
Mode: "program", Mode: "program",
Setpoint: 14, Setpoint: 14,
Time: test_time, Time: test_time,
Program_name: "default", Program_name: "default",
}, },
DeviceState{ DeviceState{
Mode: "program", Mode: "program",
Setpoint: 13, Setpoint: 13,
Time: test_time, Time: test_time,
Program_name: "other", Program_name: "other",
}, },
false, false,
}, },
{ {
DeviceState{ DeviceState{
Mode: "always", Mode: "always",
Setpoint: 14, Setpoint: 14,
Time: test_time, Time: test_time,
}, },
DeviceState{ DeviceState{
Mode: "always", Mode: "always",
Setpoint: 14, Setpoint: 14,
Time: test_time, Time: test_time,
Program_name: "other", Program_name: "other",
Until_time: test_time, Until_time: test_time,
}, },
true, true,
}, },
{ {
DeviceState{ DeviceState{
Mode: "always", Mode: "always",
Setpoint: 14, Setpoint: 14,
Time: test_time, Time: test_time,
}, },
DeviceState{ DeviceState{
Mode: "always", Mode: "always",
Setpoint: 15, Setpoint: 15,
Time: test_time, Time: test_time,
Program_name: "other", Program_name: "other",
Until_time: test_time, Until_time: test_time,
}, },
false, false,
}, },
{ {
DeviceState{ DeviceState{
Mode: "until_next", Mode: "until_next",
Setpoint: 14, Setpoint: 14,
Time: test_time, Time: test_time,
}, },
DeviceState{ DeviceState{
Mode: "until_next", Mode: "until_next",
Setpoint: 14, Setpoint: 14,
Time: test_time, Time: test_time,
Program_name: "other", Program_name: "other",
Until_time: test_time, Until_time: test_time,
}, },
true, true,
}, },
{ {
DeviceState{ DeviceState{
Mode: "until_next", Mode: "until_next",
Setpoint: 14, Setpoint: 14,
Time: test_time, Time: test_time,
}, },
DeviceState{ DeviceState{
Mode: "until_next", Mode: "until_next",
Setpoint: 13, Setpoint: 13,
Time: test_time, Time: test_time,
Program_name: "other", Program_name: "other",
Until_time: test_time, Until_time: test_time,
}, },
false, false,
}, },
{ {
DeviceState{ DeviceState{
Mode: "until_time", Mode: "until_time",
Setpoint: 14, Setpoint: 14,
Time: test_time, Time: test_time,
Until_time: test_time.Add(1*time.Hour), Until_time: test_time.Add(1 * time.Hour),
}, },
DeviceState{ DeviceState{
Mode: "until_time", Mode: "until_time",
Setpoint: 14, Setpoint: 14,
Time: test_time, Time: test_time,
Program_name: "other", Program_name: "other",
Until_time: test_time.Add(1*time.Hour), Until_time: test_time.Add(1 * time.Hour),
}, },
true, true,
}, },
{ {
DeviceState{ DeviceState{
Mode: "until_time", Mode: "until_time",
Setpoint: 14, Setpoint: 14,
Time: test_time, Time: test_time,
Until_time: test_time.Add(1*time.Hour), Until_time: test_time.Add(1 * time.Hour),
}, },
DeviceState{ DeviceState{
Mode: "until_time", Mode: "until_time",
Setpoint: 13, Setpoint: 13,
Time: test_time, Time: test_time,
Program_name: "other", Program_name: "other",
Until_time: test_time.Add(1*time.Hour), Until_time: test_time.Add(1 * time.Hour),
}, },
false, false,
}, },
{ {
DeviceState{ DeviceState{
Mode: "until_time", Mode: "until_time",
Setpoint: 14, Setpoint: 14,
Time: test_time, Time: test_time,
Until_time: test_time.Add(1*time.Hour), Until_time: test_time.Add(1 * time.Hour),
}, },
DeviceState{ DeviceState{
Mode: "until_time", Mode: "until_time",
Setpoint: 14, Setpoint: 14,
Time: test_time, Time: test_time,
Program_name: "other", Program_name: "other",
Until_time: test_time.Add(2*time.Hour), Until_time: test_time.Add(2 * time.Hour),
}, },
false, false,
}, },
} }
for i, tt := range tests { for i, tt := range tests {
testname := fmt.Sprintf("%d", i ) testname := fmt.Sprintf("%d", i)
t.Run(testname, func(t *testing.T) { t.Run(testname, func(t *testing.T) {
r := tt.state1.Equivalent(tt.state2) r := tt.state1.Equivalent(tt.state2)
if r != tt.want { if r != tt.want {
t.Errorf("got %t, want %t", r, tt.want) t.Errorf("got %t, want %t", r, tt.want)
} }
}) })
} }
} }
func TestStateTopic(t *testing.T) { func TestStateTopic(t *testing.T) {
topic := test_device.StateTopic() topic := test_device.StateTopic()
if topic != "heater/valid/state" { if topic != "heater/valid/state" {
t.Errorf("Got %s; want heater/valid/state", topic) t.Errorf("Got %s; want heater/valid/state", topic)
} }
} }
func TestListenTopic(t *testing.T) { func TestListenTopic(t *testing.T) {
topic, err := test_device.ListenTopic() topic, err := test_device.ListenTopic()
want := "zigbee2mqtt/TVR/valid" want := "zigbee2mqtt/TVR/valid"
if err != nil { if err != nil {
t.Errorf("Got %s", err.Error()) t.Errorf("Got %s", err.Error())
} }
if topic != want { if topic != want {
t.Errorf("Got %s; want %s", topic, want) t.Errorf("Got %s; want %s", topic, want)
} }
} }
func TestProgram(t *testing.T) { func TestProgram(t *testing.T) {
//case 1: no program set in state return default //case 1: no program set in state return default
case1_device := test_device case1_device := test_device
case1_device.State.Program_name = "" case1_device.State.Program_name = ""
//case 2: program set "confort" must return it //case 2: program set "confort" must return it
var test_confort_weekprogram = WeekProgram{ var test_confort_weekprogram = WeekProgram{
Monday: test_setpoints, Monday: test_setpoints,
Thuesday: test_setpoints, Thuesday: test_setpoints,
Wednesday: test_setpoints, Wednesday: test_setpoints,
Thursday : test_setpoints, Thursday: test_setpoints,
Friday: test_setpoints, Friday: test_setpoints,
Saturday: test_setpoints, Saturday: test_setpoints,
Sunday: test_setpoints, Sunday: test_setpoints,
} }
case2_device := test_device case2_device := test_device
case2_device.Settings.Programs = Programs{ case2_device.Settings.Programs = Programs{
"default": test_weekprogram, "default": test_weekprogram,
"confort": test_confort_weekprogram, "confort": test_confort_weekprogram,
} }
case2_device.State.Program_name = "confort" case2_device.State.Program_name = "confort"
//case 3: program set "confort" but not exist //case 3: program set "confort" but not exist
case3_device := test_device case3_device := test_device
case3_device.State.Program_name = "confort" case3_device.State.Program_name = "confort"
var tests = []struct { var tests = []struct {
name string name string
device Device device Device
result WeekProgram result WeekProgram
err error err error
}{ }{
{"case 1 no program set use default", case1_device, DefaultWeekProgram, nil}, {"case 1 no program set use default", case1_device, DefaultWeekProgram, nil},
{"case 2 program confort", case2_device, test_confort_weekprogram, nil}, {"case 2 program confort", case2_device, test_confort_weekprogram, nil},
{"case 3 program confort no defined", case3_device, WeekProgram{}, Error("device valid don't have confort program")}, {"case 3 program confort no defined", case3_device, WeekProgram{}, Error("device valid don't have confort program")},
} }
for _, tt := range tests { for _, tt := range tests {
testname := fmt.Sprintf("%s", tt.name ) testname := fmt.Sprintf("%s", tt.name)
t.Run(testname, func(t *testing.T) { t.Run(testname, func(t *testing.T) {
prog, err := tt.device.Program() prog, err := tt.device.Program()
if err != tt.err { if err != tt.err {
t.Errorf("got %s, want %s", err, tt.err) t.Errorf("got %s, want %s", err, tt.err)
} }
if !reflect.DeepEqual(prog,tt.result) { if !reflect.DeepEqual(prog, tt.result) {
t.Errorf("got %v, want %v", prog, tt.result) t.Errorf("got %v, want %v", prog, tt.result)
} }
}) })
} }
} }
func TestUpdate(t *testing.T) { func TestUpdate(t *testing.T) {
// device 1: currentSetpoint 0 program, not specified expect default // device 1: currentSetpoint 0 program, not specified expect default
// device 2: currentSetpoint 0 program, unknown one, expect error // device 2: currentSetpoint 0 program, unknown one, expect error
// device 3: currentSetpoint 0 always, 22 // device 3: currentSetpoint 0 always, 22
// device 4: currentSetpoint 22 until_time, fixed at timeNow, for 2h 22 // device 4: currentSetpoint 22 until_time, fixed at timeNow, for 2h 22
// device 5: currentSetpoint 17 indem 4 but fixed timeNow-2h // device 5: currentSetpoint 17 indem 4 but fixed timeNow-2h
// device 6: currentSetpoint 17, until_next set a timeNow, test time time now +1h : no change // device 6: currentSetpoint 17, until_next set a timeNow, test time time now +1h : no change
timeNow = func() time.Time {
return test_time
}
timeNow = func() time.Time { device1 := test_device
return test_time device1.Name = "1"
} device1.State = DeviceState{
Mode: "program",
Setpoint: 0,
Time: test_time,
Program_name: "",
}
device1 := test_device device2 := test_device
device1.Name = "1" device2.Name = "2"
device1.State = DeviceState{ device2.State.Program_name = "unknown"
Mode: "program",
Setpoint: 0,
Time: test_time,
Program_name: "",
}
device2 := test_device device3 := test_device
device2.Name = "2" device3.Name = "3"
device2.State.Program_name = "unknown" device3.State = DeviceState{
Mode: "always",
Setpoint: 22,
}
device3 := test_device device4 := test_device
device3.Name = "3" device4.Name = "4"
device3.State = DeviceState{ device4.CurrentSetpoint = 22
Mode: "always", device4.State = DeviceState{
Setpoint: 22, Mode: "until_time",
} Setpoint: 22,
Time: timeNow(),
Until_time: timeNow().Add(2 * time.Hour),
}
device4 := test_device device5 := test_device
device4.Name = "4" device5.Name = "5"
device4.CurrentSetpoint = 22 device5.CurrentSetpoint = 17
device4.State = DeviceState{ device5.State = DeviceState{
Mode: "until_time", Mode: "until_time",
Setpoint: 22, Setpoint: 22,
Time: timeNow(), Time: timeNow().Add(-2 * time.Hour),
Until_time: timeNow().Add(2*time.Hour), Until_time: timeNow().Add(-1 * time.Minute),
} }
device5 := test_device device6 := test_device
device5.Name = "5" device6.Name = "6"
device5.CurrentSetpoint = 17 device6.CurrentSetpoint = 22
device5.State = DeviceState{ device6.State = DeviceState{
Mode: "until_time", Mode: "until_next",
Setpoint: 22, Setpoint: 22,
Time: timeNow().Add(-2*time.Hour), Time: timeNow(),
Until_time: timeNow().Add(-1*time.Minute), }
}
device6 := test_device var tests = []struct {
device6.Name = "6" getTime func() time.Time
device6.CurrentSetpoint = 22 device Device
device6.State = DeviceState{ want []Message
Mode: "until_next", change bool
Setpoint: 22, err error
Time: timeNow(), }{
} {timeNow, device1, []Message{
{
Payload: []byte("{\"current_heating_setpoint\": 17}"),
Topic: "zigbee2mqtt/TVR/1/set",
Retain: false,
},
}, true, nil},
{timeNow, device2, []Message{}, false, Error("device 2 don't have unknown program")},
{timeNow, device3, []Message{
{
Payload: []byte("{\"current_heating_setpoint\": 22}"),
Topic: "zigbee2mqtt/TVR/3/set",
Retain: false,
},
}, true, nil},
{timeNow, device4, []Message{}, false, nil},
{timeNow, device5, []Message{}, true, nil},
{timeNow, device6, []Message{}, false, nil},
{func() time.Time { return test_time.Add(7*time.Hour + 30*time.Minute) }, device6, []Message{
{
Payload: []byte("{\"current_heating_setpoint\": 19}"),
Topic: "zigbee2mqtt/TVR/6/set",
Retain: false,
},
}, true, nil},
}
for _, tt := range tests {
testname := fmt.Sprintf("%s", tt.device.Name)
t.Run(testname, func(t *testing.T) {
var tests = []struct{ timeNow = tt.getTime
getTime func() time.Time result := []Message{}
device Device
want []Message
change bool
err error
}{
{timeNow, device1, []Message{
Message{
Payload: []byte("{\"current_heating_setpoint\": 17}"),
Topic: "zigbee2mqtt/TVR/1/set",
Retain: false,
},
}, true, nil} ,
{timeNow, device2, []Message{}, false, Error("device 2 don't have unknown program")},
{timeNow, device3, []Message{
Message{
Payload: []byte("{\"current_heating_setpoint\": 22}"),
Topic: "zigbee2mqtt/TVR/3/set",
Retain: false,
},
}, true, nil} ,
{timeNow, device4, []Message{}, false, nil} ,
{timeNow, device5, []Message{}, true, nil} ,
{timeNow, device6, []Message{}, false, nil} ,
{func()time.Time{return test_time.Add(7*time.Hour+30*time.Minute)}, device6, []Message{
Message{
Payload: []byte("{\"current_heating_setpoint\": 19}"),
Topic: "zigbee2mqtt/TVR/6/set",
Retain: false,
},
}, true, nil} ,
}
for _, tt := range tests { rchan := make(chan Message, 10)
testname := fmt.Sprintf("%s", tt.device.Name ) errchan := make(chan error, 1)
t.Run(testname, func(t *testing.T) { changechan := make(chan bool, 1)
timeNow = tt.getTime go func(result chan Message, changechan chan bool, errchan chan error) {
result := []Message{} logger := zerolog.New(ioutil.Discard).With().Timestamp().Logger()
change, err := tt.device.update(&logger, result)
errchan <- err
changechan <- change
close(rchan)
close(changechan)
close(errchan)
}(rchan, changechan, errchan)
rchan := make(chan Message, 10) for msg := range rchan {
errchan := make(chan error, 1) result = append(result, msg)
changechan := make(chan bool, 1) }
go func(result chan Message, changechan chan bool, errchan chan error) { err, ok := <-errchan
logger := zerolog.New(ioutil.Discard).With().Timestamp().Logger() if !ok {
change, err := tt.device.update(&logger, result) t.Fatal(err)
errchan <- err }
changechan <- change
close(rchan)
close(changechan)
close(errchan)
}(rchan, changechan, errchan)
for msg := range rchan { if err != tt.err {
result = append(result, msg) t.Errorf("got %s, want %s", err, tt.err)
} }
err, ok := <- errchan change, ok := <-changechan
if !ok { if !ok {
t.Fatal(err) t.Fatal(err)
} }
if err != tt.err { if change != tt.change {
t.Errorf("got %s, want %s", err, tt.err) t.Errorf("got %s, want %s", err, tt.err)
} }
change, ok := <- changechan if !reflect.DeepEqual(result, tt.want) {
if !ok { t.Errorf("got %v, want %v", result, tt.want)
t.Fatal(err) }
} })
}
if change != tt.change {
t.Errorf("got %s, want %s", err, tt.err)
}
if !reflect.DeepEqual(result,tt.want) {
t.Errorf("got %v, want %v", result, tt.want)
}
})
}
} }

View File

@ -1,256 +1,255 @@
package device package device
import ( import (
"time" "bytes"
"bytes" "fmt"
"text/template" "text/template"
"fmt" "time"
) )
type DayOfWeek int type DayOfWeek int
func (d DayOfWeek) Next() DayOfWeek { func (d DayOfWeek) Next() DayOfWeek {
if d == Sunday { if d == Sunday {
return Monday return Monday
} }
return d+1 return d + 1
} }
func (d DayOfWeek) Previous() DayOfWeek { func (d DayOfWeek) Previous() DayOfWeek {
if d == Monday { if d == Monday {
return Sunday return Sunday
} }
return d-1 return d - 1
} }
func (d DayOfWeek) DaysBetween(n DayOfWeek) int { func (d DayOfWeek) DaysBetween(n DayOfWeek) int {
var between int var between int
if (n < d) { if n < d {
between = 7 - int(d-n) between = 7 - int(d-n)
} else { } else {
between = int(n-d) between = int(n - d)
} }
return between return between
} }
const ( const (
Monday DayOfWeek = 0 Monday DayOfWeek = 0
Thuesday DayOfWeek = 1 Thuesday DayOfWeek = 1
Wednesday DayOfWeek = 2 Wednesday DayOfWeek = 2
Thursday DayOfWeek = 3 Thursday DayOfWeek = 3
Friday DayOfWeek = 4 Friday DayOfWeek = 4
Saturday DayOfWeek = 5 Saturday DayOfWeek = 5
Sunday DayOfWeek = 6 Sunday DayOfWeek = 6
) )
func WeekDayEnToFr(weekday time.Weekday) DayOfWeek { func WeekDayEnToFr(weekday time.Weekday) DayOfWeek {
// translate weekday to french week, start by Monday // translate weekday to french week, start by Monday
return map[time.Weekday]DayOfWeek { return map[time.Weekday]DayOfWeek{
time.Monday : Monday, time.Monday: Monday,
time.Tuesday : Thuesday, time.Tuesday: Thuesday,
time.Wednesday: Wednesday, time.Wednesday: Wednesday,
time.Thursday : Thursday, time.Thursday: Thursday,
time.Friday: Friday, time.Friday: Friday,
time.Saturday: Saturday, time.Saturday: Saturday,
time.Sunday: Sunday, time.Sunday: Sunday,
}[weekday] }[weekday]
} }
func daytime(t time.Time) int { func daytime(t time.Time) int {
return t.Hour()*60 + t.Minute() return t.Hour()*60 + t.Minute()
} }
func weekday(t time.Time) DayOfWeek { func weekday(t time.Time) DayOfWeek {
return WeekDayEnToFr(t.Weekday()) return WeekDayEnToFr(t.Weekday())
} }
type Message struct { type Message struct {
Payload []byte Payload []byte
Topic string Topic string
Retain bool Retain bool
} }
type Setpoint struct { type Setpoint struct {
Start int `json:"start"` Start int `json:"start"`
Preset_id int `json:"preset_id"` Preset_id int `json:"preset_id"`
} }
func (s Setpoint) Value(presets []Preset) (int, error) { func (s Setpoint) Value(presets []Preset) (int, error) {
//TODO need test //TODO need test
if len(presets) < s.Preset_id + 1 { if len(presets) < s.Preset_id+1 {
return 0, Error(fmt.Sprintf("preset id %d not found", s.Preset_id)) return 0, Error(fmt.Sprintf("preset id %d not found", s.Preset_id))
} }
return presets[s.Preset_id].Value, nil return presets[s.Preset_id].Value, nil
} }
type WeekProgram map[DayOfWeek][]Setpoint type WeekProgram map[DayOfWeek][]Setpoint
func (p WeekProgram) Current() Setpoint { func (p WeekProgram) Current() Setpoint {
// TODO: need test // TODO: need test
// return current Setpoint // return current Setpoint
now := timeNow() now := timeNow()
weekday := weekday(now) weekday := weekday(now)
daytime := daytime(now) daytime := daytime(now)
setpoint := Setpoint{} setpoint := Setpoint{}
for _, sp := range p[weekday] { for _, sp := range p[weekday] {
if daytime < sp.Start { if daytime < sp.Start {
break break
} }
setpoint = sp setpoint = sp
} }
return setpoint return setpoint
} }
func (p WeekProgram) NextTime(t time.Time) (time.Time, error) { func (p WeekProgram) NextTime(t time.Time) (time.Time, error) {
// return next program change // return next program change
weekday := weekday(t) weekday := weekday(t)
daytime := daytime(t) daytime := daytime(t)
// Recursive func to find setpoint on weekday // Recursive func to find setpoint on weekday
get := func (weekday DayOfWeek, daytime int) (Setpoint, bool) { get := func(weekday DayOfWeek, daytime int) (Setpoint, bool) {
for _, sp := range p[weekday] { for _, sp := range p[weekday] {
if daytime <= sp.Start { if daytime <= sp.Start {
return sp, true return sp, true
} }
} }
return Setpoint{}, false return Setpoint{}, false
} }
startweekday := weekday startweekday := weekday
for { for {
if setpoint, ok := get(weekday, daytime); ok { if setpoint, ok := get(weekday, daytime); ok {
next := time.Date(t.Year(), next := time.Date(t.Year(),
t.Month(), t.Month(),
t.Day() + startweekday.DaysBetween(weekday), t.Day()+startweekday.DaysBetween(weekday),
0, 0, 0, 0, 0, 0, 0, 0,
time.Local) time.Local)
next = next.Add( next = next.Add(
time.Duration( (setpoint.Start) * int(time.Minute) )) time.Duration((setpoint.Start) * int(time.Minute)))
return next, nil return next, nil
} }
weekday = weekday.Next() weekday = weekday.Next()
daytime = 0 daytime = 0
if weekday == startweekday { if weekday == startweekday {
return time.Time{}, fmt.Errorf("Shouldn't happen no setpoint found over the week") return time.Time{}, Error("shouldn't happen no setpoint found over the week")
} }
} }
} }
type Programs map[string]WeekProgram type Programs map[string]WeekProgram
type Preset struct { type Preset struct {
Label string `json:"label"` Label string `json:"label"`
Value int `json:"value"` Value int `json:"value"`
Color string `json:"color"` Color string `json:"color"`
} }
type TVRSettings struct { type TVRSettings struct {
Setpoint_topic string `json:"setpoint_topic"` Setpoint_topic string `json:"setpoint_topic"`
Setpoint_payload string `json:"setpoint_payload"` Setpoint_payload string `json:"setpoint_payload"`
Setpoint_state_topic string `json:"setpoint_state_topic"` Setpoint_state_topic string `json:"setpoint_state_topic"`
Setpoint_state_jp string `json:"setpoint_state_jp"` Setpoint_state_jp string `json:"setpoint_state_jp"`
} }
func (s TVRSettings) FormatTopicState(device_name string) (string, error) { func (s TVRSettings) FormatTopicState(device_name string) (string, error) {
type Variable struct { type Variable struct {
Device string Device string
} }
variables := Variable{device_name} variables := Variable{device_name}
t, err := template.New("topic").Parse(s.Setpoint_state_topic) t, err := template.New("topic").Parse(s.Setpoint_state_topic)
if err != nil { if err != nil {
return "", err return "", err
} }
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
err = t.Execute(buf, variables) err = t.Execute(buf, variables)
if err != nil { if err != nil {
return "", err return "", err
} }
return buf.String(), nil return buf.String(), nil
} }
func (s TVRSettings) FormatTopic(device_name string) (string, error) { func (s TVRSettings) FormatTopic(device_name string) (string, error) {
type Variable struct { type Variable struct {
Device string Device string
} }
variables := Variable{device_name} variables := Variable{device_name}
t, err := template.New("topic").Parse(s.Setpoint_topic) t, err := template.New("topic").Parse(s.Setpoint_topic)
if err != nil { if err != nil {
return "", err return "", err
} }
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
err = t.Execute(buf, variables) err = t.Execute(buf, variables)
if err != nil { if err != nil {
return "", err return "", err
} }
return buf.String(), nil return buf.String(), nil
} }
func (s TVRSettings) FormatPayload(setpoint int) (string, error) { func (s TVRSettings) FormatPayload(setpoint int) (string, error) {
type Variable struct { type Variable struct {
Setpoint int Setpoint int
} }
variables := Variable{setpoint} variables := Variable{setpoint}
t, err := template.New("payload").Parse(s.Setpoint_payload) t, err := template.New("payload").Parse(s.Setpoint_payload)
if err != nil { if err != nil {
return "", err return "", err
} }
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
err = t.Execute(buf, variables) err = t.Execute(buf, variables)
if err != nil { if err != nil {
return "", err return "", err
} }
return buf.String(), nil return buf.String(), nil
} }
type DeviceSettings struct { type DeviceSettings struct {
Programs Programs `json:"programs"` Programs Programs `json:"programs"`
Presets []Preset `json:"presets"` Presets []Preset `json:"presets"`
TVR TVRSettings `json:"TVR"` TVR TVRSettings `json:"TVR"`
} }
////////////////////////////////////////////// //////////////////////////////////////////////
// Defaults // Defaults
var DefaultPresets = []Preset{ var DefaultPresets = []Preset{
{Label: "default", Value: 17, Color: "#012a36"}, {Label: "default", Value: 17, Color: "#012a36"},
{Label: "normal", Value: 19, Color: "#b6244f"}, {Label: "normal", Value: 19, Color: "#b6244f"},
} }
var defaultSetpoints = []Setpoint{ var defaultSetpoints = []Setpoint{
{Start: 7*60, Preset_id: 1}, {Start: 7 * 60, Preset_id: 1},
{Start: 8*60, Preset_id: 0}, {Start: 8 * 60, Preset_id: 0},
{Start: 16*60, Preset_id: 1}, {Start: 16 * 60, Preset_id: 1},
{Start: 22*60, Preset_id: 0}, {Start: 22 * 60, Preset_id: 0},
} }
var DefaultWeekProgram = WeekProgram{ var DefaultWeekProgram = WeekProgram{
Monday: defaultSetpoints, Monday: defaultSetpoints,
Thuesday: defaultSetpoints, Thuesday: defaultSetpoints,
Wednesday: defaultSetpoints, Wednesday: defaultSetpoints,
Thursday : defaultSetpoints, Thursday: defaultSetpoints,
Friday: defaultSetpoints, Friday: defaultSetpoints,
Saturday: defaultSetpoints, Saturday: defaultSetpoints,
Sunday: defaultSetpoints, Sunday: defaultSetpoints,
} }
var DefaultPrograms = Programs{ var DefaultPrograms = Programs{
"default": DefaultWeekProgram, "default": DefaultWeekProgram,
} }
var DefaultTVRSettings = TVRSettings { var DefaultTVRSettings = TVRSettings{
Setpoint_topic : "zigbee2mqtt/TVR/{{.Device}}/set", Setpoint_topic: "zigbee2mqtt/TVR/{{.Device}}/set",
Setpoint_payload : "{\"current_heating_setpoint\": {{.Setpoint}}}", Setpoint_payload: "{\"current_heating_setpoint\": {{.Setpoint}}}",
Setpoint_state_topic : "zigbee2mqtt/TVR/{{.Device}}", Setpoint_state_topic: "zigbee2mqtt/TVR/{{.Device}}",
Setpoint_state_jp : "$.current_heating_setpoint", Setpoint_state_jp: "$.current_heating_setpoint",
} }
var DefaultDeviceSettings = DeviceSettings{ var DefaultDeviceSettings = DeviceSettings{
Programs: DefaultPrograms, Programs: DefaultPrograms,
Presets: DefaultPresets, Presets: DefaultPresets,
TVR: DefaultTVRSettings, TVR: DefaultTVRSettings,
} }

View File

@ -1,107 +1,104 @@
package device package device
import ( import (
"testing" "fmt"
"time" "testing"
"fmt" "time"
) )
func TestDaysBetween(t *testing.T) { func TestDaysBetween(t *testing.T) {
var tests = []struct{ var tests = []struct {
day1 DayOfWeek day1 DayOfWeek
day2 DayOfWeek day2 DayOfWeek
want int want int
}{ }{
{ Monday, Thuesday, 1 }, {Monday, Thuesday, 1},
{ Thuesday, Monday, 6 }, {Thuesday, Monday, 6},
{ Sunday, Monday, 1 }, {Sunday, Monday, 1},
{ Sunday, Thuesday, 2 }, {Sunday, Thuesday, 2},
{ Friday, Friday, 0 }, {Friday, Friday, 0},
} }
for _, tt := range tests { for _, tt := range tests {
testname := fmt.Sprintf("Days between %d and %d", tt.day1, tt.day2 ) testname := fmt.Sprintf("Days between %d and %d", tt.day1, tt.day2)
t.Run(testname, func(t *testing.T) { t.Run(testname, func(t *testing.T) {
between := tt.day1.DaysBetween(tt.day2) between := tt.day1.DaysBetween(tt.day2)
if between != tt.want { if between != tt.want {
t.Errorf("got %d, want %d", between, tt.want) t.Errorf("got %d, want %d", between, tt.want)
} }
}) })
} }
} }
func TestNextTime(t *testing.T) { func TestNextTime(t *testing.T) {
defaultSetpoints := []Setpoint{ defaultSetpoints := []Setpoint{
{Start: 7*60, Preset_id: 1}, {Start: 7 * 60, Preset_id: 1},
{Start: 8*60, Preset_id: 0}, {Start: 8 * 60, Preset_id: 0},
{Start: 16*60, Preset_id: 1}, {Start: 16 * 60, Preset_id: 1},
{Start: 22*60, Preset_id: 0}, {Start: 22 * 60, Preset_id: 0},
} }
zeroSetpoints := []Setpoint{ zeroSetpoints := []Setpoint{
{Start: 0, Preset_id: 0}, {Start: 0, Preset_id: 0},
} }
default_program := WeekProgram{
Monday: defaultSetpoints,
Thuesday: defaultSetpoints,
Wednesday: defaultSetpoints,
Thursday: defaultSetpoints,
Friday: defaultSetpoints,
Saturday: defaultSetpoints,
Sunday: defaultSetpoints,
}
default_program := WeekProgram{ zero_program := WeekProgram{
Monday: defaultSetpoints, Monday: zeroSetpoints,
Thuesday: defaultSetpoints, Thuesday: zeroSetpoints,
Wednesday: defaultSetpoints, Wednesday: zeroSetpoints,
Thursday : defaultSetpoints, Thursday: zeroSetpoints,
Friday: defaultSetpoints, Friday: zeroSetpoints,
Saturday: defaultSetpoints, Saturday: zeroSetpoints,
Sunday: defaultSetpoints, Sunday: zeroSetpoints,
} }
zero_program := WeekProgram{ var tests = []struct {
Monday: zeroSetpoints, prog WeekProgram
Thuesday: zeroSetpoints, time time.Time
Wednesday: zeroSetpoints, want time.Time
Thursday : zeroSetpoints, }{
Friday: zeroSetpoints, {
Saturday: zeroSetpoints, default_program,
Sunday: zeroSetpoints, time.Date(2022, time.October, 23, 9, 0, 0, 0, time.Local),
} time.Date(2022, time.October, 23, 16, 0, 0, 0, time.Local),
},
{
var tests = []struct{ default_program,
prog WeekProgram time.Date(2022, time.October, 24, 5, 0, 0, 0, time.Local),
time time.Time time.Date(2022, time.October, 24, 7, 0, 0, 0, time.Local),
want time.Time },
}{ {
{ default_program,
default_program, time.Date(2022, time.October, 23, 23, 0, 0, 0, time.Local),
time.Date(2022, time.October, 23, 9, 0, 0, 0, time.Local), time.Date(2022, time.October, 24, 7, 0, 0, 0, time.Local),
time.Date(2022, time.October, 23, 16, 0, 0, 0, time.Local), },
}, {
{ zero_program,
default_program, time.Date(2022, time.October, 23, 23, 0, 0, 0, time.Local),
time.Date(2022, time.October, 24, 5, 0, 0, 0, time.Local), time.Date(2022, time.October, 24, 0, 0, 0, 0, time.Local),
time.Date(2022, time.October, 24, 7, 0, 0, 0, time.Local), },
}, }
{ for _, tt := range tests {
default_program, testname := fmt.Sprintf("%s", tt.time.String())
time.Date(2022, time.October, 23, 23, 0, 0, 0, time.Local), t.Run(testname, func(t *testing.T) {
time.Date(2022, time.October, 24, 7, 0, 0, 0, time.Local), next, err := tt.prog.NextTime(tt.time)
}, if err != nil {
{ t.Fatalf(err.Error())
zero_program, }
time.Date(2022, time.October, 23, 23, 0, 0, 0, time.Local), if !next.Equal(tt.want) {
time.Date(2022, time.October, 24, 0, 0, 0, 0, time.Local), t.Errorf("got %s, want %s", next.String(), tt.want.String())
}, }
} })
for _, tt := range tests { }
testname := fmt.Sprintf("%s", tt.time.String() )
t.Run(testname, func(t *testing.T) {
next, err := tt.prog.NextTime(tt.time)
if err != nil {
t.Fatalf(err.Error())
}
if !next.Equal(tt.want) {
t.Errorf("got %s, want %s", next.String(), tt.want.String())
}
})
}
} }