381 lines
8.2 KiB
Go
381 lines
8.2 KiB
Go
package device
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"git.quimbo.fr/nicolas/mqtt"
|
|
"github.com/ohler55/ojg/oj"
|
|
"github.com/rs/zerolog"
|
|
// "reflect"
|
|
)
|
|
|
|
var timeNow = func() time.Time {
|
|
return time.Now()
|
|
}
|
|
|
|
type Error string
|
|
|
|
func (e Error) Error() string { return string(e) }
|
|
|
|
type DeviceState struct {
|
|
Mode string `json:"mode"`
|
|
Setpoint int `json:"setpoint"`
|
|
Time time.Time `json:"time"`
|
|
ProgramName string `json:"program_name"`
|
|
UntilTime time.Time `json:"until_time"`
|
|
ValveState string `json:"valve_state"`
|
|
LocalTemperature int `json:"local_temperature"`
|
|
}
|
|
|
|
func (s *DeviceState) Equivalent(state DeviceState) bool {
|
|
if state.Mode != s.Mode {
|
|
return false
|
|
}
|
|
if !state.Time.Equal(s.Time) {
|
|
return false
|
|
}
|
|
|
|
switch state.Mode {
|
|
case "always":
|
|
if state.Setpoint != s.Setpoint {
|
|
return false
|
|
}
|
|
case "until_next":
|
|
if state.Setpoint != s.Setpoint {
|
|
return false
|
|
}
|
|
case "until_time":
|
|
if state.Setpoint != s.Setpoint {
|
|
return false
|
|
}
|
|
if !state.UntilTime.Equal(s.UntilTime) {
|
|
return false
|
|
}
|
|
case "program":
|
|
if state.ProgramName != s.ProgramName {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// Internal state
|
|
type Device struct {
|
|
Name string
|
|
Settings DeviceSettings
|
|
CurrentSetpoint int
|
|
State DeviceState
|
|
}
|
|
|
|
func (d *Device) StateTopic() string {
|
|
return fmt.Sprintf("heater/%s/state", d.Name)
|
|
}
|
|
|
|
func (d Device) ListenTopic() (string, error) {
|
|
return d.Settings.TVR.FormatTopicState(d.Name)
|
|
}
|
|
|
|
func (d *Device) Program() (WeekProgram, error) {
|
|
// return current device program if specified or default one
|
|
prog_name := "default"
|
|
if d.State.ProgramName != "" {
|
|
prog_name = d.State.ProgramName
|
|
}
|
|
|
|
program, ok := d.Settings.Programs[prog_name]
|
|
if !ok {
|
|
return WeekProgram{}, Error(fmt.Sprintf("device %s don't have %s program", d.Name, prog_name))
|
|
}
|
|
|
|
return program, nil
|
|
}
|
|
|
|
func (d *Device) ProgramName() string {
|
|
if d.State.ProgramName != "" {
|
|
return d.State.ProgramName
|
|
}
|
|
return "default"
|
|
}
|
|
|
|
func (d *Device) publishState(pubchan chan Message) error {
|
|
|
|
payload, err := json.Marshal(d.State)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pubchan <- Message{
|
|
Topic: d.StateTopic(),
|
|
Payload: payload,
|
|
Retain: true,
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *Device) SetSetpoint(value int, pubchan chan Message) error {
|
|
topic, err := d.Settings.TVR.FormatTopic(d.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
payload, err := d.Settings.TVR.FormatPayload(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pubchan <- Message{
|
|
Topic: topic,
|
|
Payload: []byte(payload),
|
|
Retain: false,
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *Device) SetState(log zerolog.Logger, state DeviceState, pubchan chan Message) error {
|
|
// Called on mqtt state/set cmd received
|
|
// If same state do nothing
|
|
if d.State.Equivalent(state) {
|
|
log.Debug().Msg("same state no change")
|
|
return nil
|
|
}
|
|
// keep tvr state
|
|
state.LocalTemperature = d.State.LocalTemperature
|
|
state.ValveState = d.State.ValveState
|
|
|
|
d.State = state
|
|
|
|
// ignore change info, we already known
|
|
_, err := d.update(log, pubchan)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return d.publishState(pubchan)
|
|
}
|
|
|
|
func (d *Device) CheckSetpoint(log zerolog.Logger, pubchan chan Message) error {
|
|
change, err := d.update(log, pubchan)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if change {
|
|
return d.publishState(pubchan)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *Device) update(log zerolog.Logger, pubchan chan Message) (bool, error) {
|
|
// Handle all the update setpoint logic
|
|
// push in pubChan a Message if setpoint need to be update
|
|
log = log.With().
|
|
Str("device", d.Name).
|
|
Int("current_setpoint", d.CurrentSetpoint).
|
|
Str("State.Mode", d.State.Mode).
|
|
Int("State.Setpoint", d.State.Setpoint).
|
|
Str("State.Program_name", d.State.ProgramName).
|
|
Logger()
|
|
log.Debug().Msg("check if setpoint need an update")
|
|
|
|
switch d.State.Mode {
|
|
case "always":
|
|
return d.handle_always(log, pubchan)
|
|
|
|
case "until_time":
|
|
return d.handle_until_time(log, pubchan)
|
|
|
|
case "until_next":
|
|
return d.handle_until_next(log, pubchan)
|
|
case "program":
|
|
return d.handle_program(log, pubchan)
|
|
default:
|
|
log.Info().Msg("Use default mode")
|
|
return d.handle_reset_state(log, pubchan)
|
|
}
|
|
|
|
}
|
|
|
|
func (d *Device) onMessage(ctx context.Context, msg mqtt.Message) {
|
|
log := zerolog.Ctx(ctx).With().
|
|
Str("action", "device state receive").
|
|
Str("device", d.Name).
|
|
Logger()
|
|
|
|
log.Debug().
|
|
Str("topic", msg.Topic()).
|
|
Str("payload", string(msg.Payload())).
|
|
Msg("Message get")
|
|
|
|
obj, err := oj.ParseString(string(msg.Payload()))
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("during payload parse")
|
|
return
|
|
}
|
|
// Setpoint
|
|
setpoint, err := d.Settings.TVR.ParseSetpoint(obj)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("while parsing setpoint")
|
|
return
|
|
}
|
|
d.CurrentSetpoint = setpoint
|
|
|
|
// LocalTemperature
|
|
localTemperature, err := d.Settings.TVR.ParseLocalTemperature(obj)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("while parsing LocalTemperature")
|
|
return
|
|
}
|
|
d.State.LocalTemperature = localTemperature
|
|
|
|
// Valve
|
|
valve, err := d.Settings.TVR.ParseValve(obj)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("while parsing Valve")
|
|
return
|
|
}
|
|
d.State.ValveState = valve
|
|
|
|
if v := ctx.Value("pubchan"); v != nil {
|
|
if pubchan, ok := v.(chan Message); ok {
|
|
d.publishState(pubchan)
|
|
} else {
|
|
log.Error().Msg("invalid pubchan ctx type")
|
|
}
|
|
} else {
|
|
log.Error().Msg("pubchan not found in ctx")
|
|
}
|
|
|
|
}
|
|
|
|
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
|
|
program, err := d.Program()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
current_setpoint := program.Current()
|
|
|
|
value, err := current_setpoint.Value(d.Settings.Presets)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
d.State = DeviceState{
|
|
Setpoint: value,
|
|
Mode: "program",
|
|
ProgramName: d.ProgramName(),
|
|
Time: timeNow(),
|
|
}
|
|
|
|
if d.CurrentSetpoint != value {
|
|
log.Info().Msg("publish setpoint update")
|
|
err = d.SetSetpoint(value, pubchan)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (d *Device) handle_always(log zerolog.Logger, pubchan chan Message) (bool, error) {
|
|
// return true if change made
|
|
|
|
if d.State.Setpoint != d.CurrentSetpoint {
|
|
if err := d.SetSetpoint(d.State.Setpoint, pubchan); err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
log.Info().Msg("no setpoint update")
|
|
return false, nil
|
|
}
|
|
|
|
func (d *Device) handle_until_time(log zerolog.Logger, pubchan chan Message) (bool, error) {
|
|
log = log.With().Time("until_time", d.State.UntilTime).Logger()
|
|
|
|
if d.State.UntilTime.Before(timeNow()) {
|
|
log.Info().Msg("until_time passed, reset")
|
|
return d.handle_reset_state(log, pubchan)
|
|
}
|
|
|
|
if d.State.Setpoint != d.CurrentSetpoint {
|
|
log.Info().Msg("need setpoint update")
|
|
if err := d.SetSetpoint(d.State.Setpoint, pubchan); err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
log.Info().Msg("no setpoint update")
|
|
return false, nil
|
|
}
|
|
|
|
func (d *Device) handle_until_next(log zerolog.Logger, pubchan chan Message) (bool, error) {
|
|
log = log.With().Time("until_next", d.State.Time).Logger()
|
|
|
|
program, err := d.Program()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
next, err := program.NextTime(d.State.Time)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if timeNow().After(next) {
|
|
// force current program
|
|
// reset state
|
|
log.Info().Time("now", timeNow()).Time("next", next).Msg("until_next expired")
|
|
return d.handle_reset_state(log, pubchan)
|
|
}
|
|
|
|
if d.State.Setpoint != d.CurrentSetpoint {
|
|
log.Info().Msg("need setpoint update")
|
|
if err := d.SetSetpoint(d.State.Setpoint, pubchan); err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
log.Info().Msg("no setpoint update")
|
|
return false, nil
|
|
}
|
|
|
|
func (d *Device) handle_program(log zerolog.Logger, pubchan chan Message) (bool, error) {
|
|
log = log.With().Str("program", d.State.ProgramName).Logger()
|
|
|
|
program, err := d.Program()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
current_setpoint := program.Current()
|
|
|
|
value, err := current_setpoint.Value(d.Settings.Presets)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
d.State.Setpoint = value
|
|
|
|
if d.CurrentSetpoint != value {
|
|
log.Info().Msg("publish setpoint update")
|
|
if err := d.SetSetpoint(value, pubchan); err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
log.Info().Msg("no setpoint update")
|
|
return false, nil
|
|
}
|