Use set state topic, improve update logic and test
This commit is contained in:
parent
86175870b0
commit
30b3e4417b
3
go.mod
3
go.mod
@ -12,7 +12,8 @@ require (
|
|||||||
github.com/ohler55/ojg v1.14.5 // indirect
|
github.com/ohler55/ojg v1.14.5 // indirect
|
||||||
github.com/rs/zerolog v1.28.0 // indirect
|
github.com/rs/zerolog v1.28.0 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||||
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b // indirect
|
golang.org/x/net v0.1.0 // indirect
|
||||||
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde // indirect
|
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde // indirect
|
||||||
golang.org/x/sys v0.1.0 // indirect
|
golang.org/x/sys v0.1.0 // indirect
|
||||||
|
golang.org/x/tools v0.2.0 // indirect
|
||||||
)
|
)
|
||||||
|
4
go.sum
4
go.sum
@ -34,6 +34,8 @@ golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbP
|
|||||||
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY=
|
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY=
|
||||||
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||||
|
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
|
||||||
|
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde h1:ejfdSekXMDxDLbRrJMwUk6KnSLZ2McaUCVcIKM+N6jc=
|
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde h1:ejfdSekXMDxDLbRrJMwUk6KnSLZ2McaUCVcIKM+N6jc=
|
||||||
@ -50,5 +52,7 @@ golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE=
|
||||||
|
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
26
main.go
26
main.go
@ -148,12 +148,12 @@ func (app *App) onSettingsMessage(ctx context.Context, msg mqttpaho.Message) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) onStateMessage(ctx context.Context, msg mqttpaho.Message) {
|
func (app *App) onSetStateMessage(ctx context.Context, msg mqttpaho.Message) {
|
||||||
// callback for topic /{prefix}/{tvr_id}/state 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", "onStateMessage").
|
Str("action", "onSetStateMessage").
|
||||||
Str("device", device_name).
|
Str("device", device_name).
|
||||||
Logger()
|
Logger()
|
||||||
ctx = logger.WithContext(ctx)
|
ctx = logger.WithContext(ctx)
|
||||||
@ -164,26 +164,22 @@ func (app *App) onStateMessage(ctx context.Context, msg mqttpaho.Message) {
|
|||||||
logger.Error().Err(err).Msg("Error while parsing payload")
|
logger.Error().Err(err).Msg("Error while parsing payload")
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := app.DeviceManager.Get(device_name); !ok {
|
logger.Info().Interface("state", state).Msg("new state")
|
||||||
tvr := device.Device{
|
|
||||||
Name: device_name,
|
|
||||||
State: state,
|
|
||||||
}
|
|
||||||
app.DeviceManager.Add(tvr)
|
|
||||||
}
|
|
||||||
device, ok := app.DeviceManager.Get(device_name)
|
device, ok := app.DeviceManager.Get(device_name)
|
||||||
if !ok {
|
if !ok {
|
||||||
logger.Error().Msg("Device not found")
|
logger.Error().Msg("Device not found, abord")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Warn().Interface("state", state).Interface("device_state", device.State).Msg("")
|
|
||||||
|
|
||||||
if !device.SameState(state) {
|
if !device.SameState(state) {
|
||||||
device.State = state
|
device.State = state
|
||||||
|
|
||||||
err := app.DeviceManager.Check(ctx, device_name)
|
err := app.DeviceManager.Check(ctx, device_name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error().Err(err).Msg("Error while checking device")
|
logger.Error().Err(err).Msg("error while checking device")
|
||||||
}
|
}
|
||||||
|
// device.SetState(log, state, pubchan)
|
||||||
} else {
|
} else {
|
||||||
logger.Info().Msg("no state update, ignoring")
|
logger.Info().Msg("no state update, ignoring")
|
||||||
}
|
}
|
||||||
@ -210,7 +206,7 @@ func (app *App) Run() {
|
|||||||
app.ctx = ctx
|
app.ctx = ctx
|
||||||
|
|
||||||
app.subscribe("heater/+/settings", 2, app.onSettingsMessage)
|
app.subscribe("heater/+/settings", 2, app.onSettingsMessage)
|
||||||
app.subscribe("heater/+/state", 2, app.onStateMessage)
|
app.subscribe("heater/+/state/set", 2, app.onSetStateMessage)
|
||||||
|
|
||||||
go func(ctx context.Context, pub *mqtt.Publisher) {
|
go func(ctx context.Context, pub *mqtt.Publisher) {
|
||||||
log := zerolog.Ctx(ctx).With().
|
log := zerolog.Ctx(ctx).With().
|
||||||
|
@ -17,10 +17,10 @@ var timeNow = func() time.Time {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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"`
|
||||||
@ -29,6 +29,40 @@ type DeviceState struct {
|
|||||||
Until_time time.Time `json:"until_time"`
|
Until_time time.Time `json:"until_time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.Until_time.Equal(s.Until_time) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case "program":
|
||||||
|
if state.Program_name != s.Program_name {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Internal state
|
// Internal state
|
||||||
type Device struct {
|
type Device struct {
|
||||||
Name string
|
Name string
|
||||||
@ -37,24 +71,6 @@ type Device struct {
|
|||||||
State DeviceState
|
State DeviceState
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Device) SameState(state DeviceState) bool {
|
|
||||||
if state.Mode != d.State.Mode {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if state.Setpoint != d.State.Setpoint {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if state.Program_name != d.State.Program_name {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !state.Time.Equal(d.State.Time) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !state.Until_time.Equal(d.State.Until_time) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
||||||
@ -64,6 +80,7 @@ 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"
|
||||||
@ -79,6 +96,7 @@ func (d *Device) Program() (WeekProgram, error) {
|
|||||||
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 != "" {
|
||||||
@ -87,9 +105,10 @@ func (d *Device) ProgramName() (string) {
|
|||||||
return prog_name
|
return prog_name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Device) SetState(state DeviceState, pubchan chan Message) error {
|
|
||||||
|
|
||||||
payload, err := json.Marshal(state)
|
func (d *Device) publishState(pubchan chan Message) error {
|
||||||
|
|
||||||
|
payload, err := json.Marshal(d.State)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -100,11 +119,10 @@ func (d *Device) SetState(state DeviceState, pubchan chan Message) error {
|
|||||||
Retain: true,
|
Retain: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
d.State = state
|
|
||||||
|
|
||||||
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 {
|
||||||
@ -125,10 +143,50 @@ func (d *Device) SetSetpoint(value int, pubchan chan Message) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (d *Device) CheckSetpoint(logger *zerolog.Logger, pubchan chan Message) error {
|
func (d *Device) SetState(log *zerolog.Logger, state DeviceState, pubchan chan Message) error {
|
||||||
|
// If same state do nothing
|
||||||
|
// else use checksetpoint for changing state
|
||||||
|
if d.State.Equivalent(state) {
|
||||||
|
log.Debug().Msg("same state no change")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
d.State = state
|
||||||
|
|
||||||
|
// ignore change info, we already known
|
||||||
|
_, err := d.update(log, pubchan)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := d.publishState(pubchan); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (d *Device) CheckSetpoint(log *zerolog.Logger, pubchan chan Message) error {
|
||||||
|
change, err := d.update(log, pubchan)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if change {
|
||||||
|
if err := d.publishState(pubchan); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 := logger.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).
|
||||||
@ -139,55 +197,22 @@ func (d *Device) CheckSetpoint(logger *zerolog.Logger, pubchan chan Message) err
|
|||||||
|
|
||||||
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.setProgAndReset(&log, pubchan)
|
return d.handle_reset_state(log, pubchan)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Device) setProgAndReset(log *zerolog.Logger, pubchan chan Message) error {
|
|
||||||
program, err := d.Program()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
current_setpoint := program.Current()
|
|
||||||
|
|
||||||
value, err := current_setpoint.Value(d.Settings.Presets)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if d.CurrentSetpoint != value {
|
|
||||||
log.Info().Msg("publish setpoint update")
|
|
||||||
err = d.SetSetpoint(value, pubchan)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Info().Msg("no setpoint update")
|
|
||||||
}
|
|
||||||
|
|
||||||
state := DeviceState{
|
|
||||||
Setpoint: value,
|
|
||||||
Mode: "program",
|
|
||||||
Program_name: d.ProgramName(),
|
|
||||||
Time: timeNow(),
|
|
||||||
}
|
|
||||||
|
|
||||||
return d.SetState(state, 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().
|
||||||
@ -223,82 +248,129 @@ func (d *Device) onMessage(ctx context.Context, msg mqtt.Message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (d *Device) handle_always(log *zerolog.Logger, pubchan chan Message) error {
|
func (d *Device) handle_reset_state(log *zerolog.Logger, pubchan chan Message) (bool, error) {
|
||||||
if d.State.Setpoint != d.CurrentSetpoint {
|
// called when need to fallback to program mode previous or default
|
||||||
return d.SetSetpoint(d.State.Setpoint, pubchan)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Device) handle_until_time(log *zerolog.Logger, pubchan chan Message) error {
|
|
||||||
*log = log.With().Time("until_time", d.State.Until_time).Logger()
|
|
||||||
|
|
||||||
if d.State.Until_time.Before(timeNow()) {
|
|
||||||
log.Info().Msg("until_time passed, reset")
|
|
||||||
return d.setProgAndReset(log, pubchan)
|
|
||||||
}
|
|
||||||
|
|
||||||
if d.State.Setpoint != d.CurrentSetpoint {
|
|
||||||
log.Info().Msg("need setpoint update")
|
|
||||||
return d.SetSetpoint(d.State.Setpoint, pubchan)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Device) handle_until_next(log *zerolog.Logger, pubchan chan Message) error {
|
|
||||||
*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 err
|
return false, err
|
||||||
}
|
|
||||||
|
|
||||||
next, err := program.NextTime(d.State.Time)
|
|
||||||
if err!= nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if timeNow().After(next) {
|
|
||||||
// force current program
|
|
||||||
// reset state
|
|
||||||
log.Info().Time("now", timeNow()).Time("next", next).Msg("until_next expired")
|
|
||||||
return d.setProgAndReset(log, pubchan)
|
|
||||||
}
|
|
||||||
|
|
||||||
if d.State.Setpoint != d.CurrentSetpoint {
|
|
||||||
log.Info().Msg("need setpoint update")
|
|
||||||
return d.SetSetpoint(d.State.Setpoint, pubchan)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Device) handle_program(log *zerolog.Logger, pubchan chan Message) error {
|
|
||||||
*log = log.With().Str("program", d.State.Program_name).Logger()
|
|
||||||
|
|
||||||
program, err := d.Program()
|
|
||||||
if err != nil {
|
|
||||||
return 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 err
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.State = DeviceState{
|
||||||
|
Setpoint: value,
|
||||||
|
Mode: "program",
|
||||||
|
Program_name: d.ProgramName(),
|
||||||
|
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 err
|
return false, err
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
log.Info().Msg("no setpoint update")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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.Until_time).Logger()
|
||||||
|
|
||||||
|
if d.State.Until_time.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.Program_name).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
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.CurrentSetpoint != value {
|
||||||
|
log.Info().Msg("publish setpoint update")
|
||||||
|
if err := d.SetSetpoint(value, pubchan); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
d.State.Setpoint = value
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Msg("no setpoint update")
|
||||||
|
return false, nil
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,6 @@ import (
|
|||||||
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"},
|
||||||
@ -57,6 +55,192 @@ var test_device = Device {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStateEquivalent(t *testing.T) {
|
||||||
|
|
||||||
|
var tests = []struct{
|
||||||
|
state1 DeviceState
|
||||||
|
state2 DeviceState
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
DeviceState{
|
||||||
|
Mode: "program",
|
||||||
|
Setpoint: 14,
|
||||||
|
Time: test_time,
|
||||||
|
Program_name: "default",
|
||||||
|
},
|
||||||
|
DeviceState{
|
||||||
|
Mode: "always",
|
||||||
|
Setpoint: 13,
|
||||||
|
Time: test_time,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DeviceState{
|
||||||
|
Mode: "program",
|
||||||
|
Setpoint: 14,
|
||||||
|
Time: test_time,
|
||||||
|
Program_name: "default",
|
||||||
|
},
|
||||||
|
DeviceState{
|
||||||
|
Mode: "program",
|
||||||
|
Setpoint: 14,
|
||||||
|
Time: test_time.Add(1*time.Minute),
|
||||||
|
Program_name: "default",
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DeviceState{
|
||||||
|
Mode: "program",
|
||||||
|
Setpoint: 14,
|
||||||
|
Time: test_time,
|
||||||
|
Program_name: "default",
|
||||||
|
},
|
||||||
|
DeviceState{
|
||||||
|
Mode: "program",
|
||||||
|
Setpoint: 13,
|
||||||
|
Time: test_time,
|
||||||
|
Program_name: "default",
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DeviceState{
|
||||||
|
Mode: "program",
|
||||||
|
Setpoint: 14,
|
||||||
|
Time: test_time,
|
||||||
|
Program_name: "default",
|
||||||
|
},
|
||||||
|
DeviceState{
|
||||||
|
Mode: "program",
|
||||||
|
Setpoint: 13,
|
||||||
|
Time: test_time,
|
||||||
|
Program_name: "other",
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DeviceState{
|
||||||
|
Mode: "always",
|
||||||
|
Setpoint: 14,
|
||||||
|
Time: test_time,
|
||||||
|
},
|
||||||
|
DeviceState{
|
||||||
|
Mode: "always",
|
||||||
|
Setpoint: 14,
|
||||||
|
Time: test_time,
|
||||||
|
Program_name: "other",
|
||||||
|
Until_time: test_time,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DeviceState{
|
||||||
|
Mode: "always",
|
||||||
|
Setpoint: 14,
|
||||||
|
Time: test_time,
|
||||||
|
},
|
||||||
|
DeviceState{
|
||||||
|
Mode: "always",
|
||||||
|
Setpoint: 15,
|
||||||
|
Time: test_time,
|
||||||
|
Program_name: "other",
|
||||||
|
Until_time: test_time,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DeviceState{
|
||||||
|
Mode: "until_next",
|
||||||
|
Setpoint: 14,
|
||||||
|
Time: test_time,
|
||||||
|
},
|
||||||
|
DeviceState{
|
||||||
|
Mode: "until_next",
|
||||||
|
Setpoint: 14,
|
||||||
|
Time: test_time,
|
||||||
|
Program_name: "other",
|
||||||
|
Until_time: test_time,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DeviceState{
|
||||||
|
Mode: "until_next",
|
||||||
|
Setpoint: 14,
|
||||||
|
Time: test_time,
|
||||||
|
},
|
||||||
|
DeviceState{
|
||||||
|
Mode: "until_next",
|
||||||
|
Setpoint: 13,
|
||||||
|
Time: test_time,
|
||||||
|
Program_name: "other",
|
||||||
|
Until_time: test_time,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DeviceState{
|
||||||
|
Mode: "until_time",
|
||||||
|
Setpoint: 14,
|
||||||
|
Time: test_time,
|
||||||
|
Until_time: test_time.Add(1*time.Hour),
|
||||||
|
},
|
||||||
|
DeviceState{
|
||||||
|
Mode: "until_time",
|
||||||
|
Setpoint: 14,
|
||||||
|
Time: test_time,
|
||||||
|
Program_name: "other",
|
||||||
|
Until_time: test_time.Add(1*time.Hour),
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DeviceState{
|
||||||
|
Mode: "until_time",
|
||||||
|
Setpoint: 14,
|
||||||
|
Time: test_time,
|
||||||
|
Until_time: test_time.Add(1*time.Hour),
|
||||||
|
},
|
||||||
|
DeviceState{
|
||||||
|
Mode: "until_time",
|
||||||
|
Setpoint: 13,
|
||||||
|
Time: test_time,
|
||||||
|
Program_name: "other",
|
||||||
|
Until_time: test_time.Add(1*time.Hour),
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DeviceState{
|
||||||
|
Mode: "until_time",
|
||||||
|
Setpoint: 14,
|
||||||
|
Time: test_time,
|
||||||
|
Until_time: test_time.Add(1*time.Hour),
|
||||||
|
},
|
||||||
|
DeviceState{
|
||||||
|
Mode: "until_time",
|
||||||
|
Setpoint: 14,
|
||||||
|
Time: test_time,
|
||||||
|
Program_name: "other",
|
||||||
|
Until_time: test_time.Add(2*time.Hour),
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i, tt := range tests {
|
||||||
|
testname := fmt.Sprintf("%d", i )
|
||||||
|
t.Run(testname, func(t *testing.T) {
|
||||||
|
r := tt.state1.Equivalent(tt.state2)
|
||||||
|
if 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()
|
||||||
@ -65,6 +249,17 @@ func TestStateTopic(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestListenTopic(t *testing.T) {
|
||||||
|
topic, err := test_device.ListenTopic()
|
||||||
|
want := "zigbee2mqtt/TVR/valid"
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Got %s", err.Error())
|
||||||
|
}
|
||||||
|
if 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
|
||||||
@ -116,7 +311,13 @@ func TestProgram(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func TestCheckSetpoint(t *testing.T) {
|
func TestUpdate(t *testing.T) {
|
||||||
|
// device 1: currentSetpoint 0 program, not specified expect default
|
||||||
|
// device 2: currentSetpoint 0 program, unknown one, expect error
|
||||||
|
// device 3: currentSetpoint 0 always, 22
|
||||||
|
// device 4: currentSetpoint 22 until_time, fixed at timeNow, for 2h 22
|
||||||
|
// device 5: currentSetpoint 17 indem 4 but fixed timeNow-2h
|
||||||
|
|
||||||
timeNow = func() time.Time {
|
timeNow = func() time.Time {
|
||||||
return test_time
|
return test_time
|
||||||
}
|
}
|
||||||
@ -134,17 +335,56 @@ func TestCheckSetpoint(t *testing.T) {
|
|||||||
device2.Name = "2"
|
device2.Name = "2"
|
||||||
device2.State.Program_name = "unknown"
|
device2.State.Program_name = "unknown"
|
||||||
|
|
||||||
|
device3 := test_device
|
||||||
|
device3.Name = "3"
|
||||||
|
device3.State = DeviceState{
|
||||||
|
Mode: "always",
|
||||||
|
Setpoint: 22,
|
||||||
|
}
|
||||||
|
|
||||||
|
device4 := test_device
|
||||||
|
device4.Name = "4"
|
||||||
|
device4.CurrentSetpoint = 22
|
||||||
|
device4.State = DeviceState{
|
||||||
|
Mode: "until_time",
|
||||||
|
Setpoint: 22,
|
||||||
|
Time: timeNow(),
|
||||||
|
Until_time: timeNow().Add(2*time.Hour),
|
||||||
|
}
|
||||||
|
|
||||||
|
device5 := test_device
|
||||||
|
device5.Name = "5"
|
||||||
|
device5.CurrentSetpoint = 17
|
||||||
|
device5.State = DeviceState{
|
||||||
|
Mode: "until_time",
|
||||||
|
Setpoint: 22,
|
||||||
|
Time: timeNow().Add(-2*time.Hour),
|
||||||
|
Until_time: timeNow().Add(-1*time.Minute),
|
||||||
|
}
|
||||||
|
|
||||||
var tests = []struct{
|
var tests = []struct{
|
||||||
device Device
|
device Device
|
||||||
want []Message
|
want []Message
|
||||||
|
change bool
|
||||||
err error
|
err error
|
||||||
}{
|
}{
|
||||||
{device1, []Message{Message{
|
{device1, []Message{
|
||||||
|
Message{
|
||||||
Payload: []byte("{\"current_heating_setpoint\": 17}"),
|
Payload: []byte("{\"current_heating_setpoint\": 17}"),
|
||||||
Topic: "zigbee2mqtt/TVR/1/set",
|
Topic: "zigbee2mqtt/TVR/1/set",
|
||||||
Retain: false,
|
Retain: false,
|
||||||
}}, nil} ,
|
},
|
||||||
{device2, []Message{}, Error("device 2 don't have unknown program")},
|
}, true, nil} ,
|
||||||
|
{device2, []Message{}, false, Error("device 2 don't have unknown program")},
|
||||||
|
{device3, []Message{
|
||||||
|
Message{
|
||||||
|
Payload: []byte("{\"current_heating_setpoint\": 22}"),
|
||||||
|
Topic: "zigbee2mqtt/TVR/3/set",
|
||||||
|
Retain: false,
|
||||||
|
},
|
||||||
|
}, true, nil} ,
|
||||||
|
{device4, []Message{}, false, nil} ,
|
||||||
|
{device5, []Message{}, true, nil} ,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@ -155,13 +395,17 @@ func TestCheckSetpoint(t *testing.T) {
|
|||||||
|
|
||||||
rchan := make(chan Message, 10)
|
rchan := make(chan Message, 10)
|
||||||
errchan := make(chan error, 1)
|
errchan := make(chan error, 1)
|
||||||
|
changechan := make(chan bool, 1)
|
||||||
|
|
||||||
go func(result chan Message, errchan chan error) {
|
go func(result chan Message, changechan chan bool, errchan chan error) {
|
||||||
logger := zerolog.New(ioutil.Discard).With().Timestamp().Logger()
|
logger := zerolog.New(ioutil.Discard).With().Timestamp().Logger()
|
||||||
errchan <- tt.device.CheckSetpoint(&logger, result)
|
change, err := tt.device.update(&logger, result)
|
||||||
|
errchan <- err
|
||||||
|
changechan <- change
|
||||||
close(rchan)
|
close(rchan)
|
||||||
|
close(changechan)
|
||||||
close(errchan)
|
close(errchan)
|
||||||
}(rchan, errchan)
|
}(rchan, changechan, errchan)
|
||||||
|
|
||||||
for msg := range rchan {
|
for msg := range rchan {
|
||||||
result = append(result, msg)
|
result = append(result, msg)
|
||||||
@ -176,6 +420,15 @@ func TestCheckSetpoint(t *testing.T) {
|
|||||||
t.Errorf("got %s, want %s", err, tt.err)
|
t.Errorf("got %s, want %s", err, tt.err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
change, ok := <- changechan
|
||||||
|
if !ok {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if change != tt.change {
|
||||||
|
t.Errorf("got %s, want %s", err, tt.err)
|
||||||
|
}
|
||||||
|
|
||||||
if !reflect.DeepEqual(result,tt.want) {
|
if !reflect.DeepEqual(result,tt.want) {
|
||||||
t.Errorf("got %v, want %v", result, tt.want)
|
t.Errorf("got %v, want %v", result, tt.want)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user