diff --git a/pkg/device/device.go b/pkg/device/device.go index afdd603..309949c 100644 --- a/pkg/device/device.go +++ b/pkg/device/device.go @@ -12,6 +12,15 @@ import ( // "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"` @@ -51,113 +60,34 @@ func (d *Device) StateTopic() string { return fmt.Sprintf("heater/%s/state", d.Name) } -func (d *Device) CheckSetpoint(logger *zerolog.Logger, pubchan chan Message) error { - // Handle all the update setpoint logic - // push in pubChan a Message if setpoint need to be update - log := logger.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.Program_name). - Logger() - log.Info().Msg("Check if setpoint need an update") - - switch d.State.Mode { - case "always": - log.Info().Msg("Use always") - - case "until_time": - log.Info().Msg("Use until_time") - if d.State.Until_time.Before(time.Now()) { - log.Info().Time("until_time", d.State.Until_time). - Msg("until_time passed, reset") - return d.setProgAndReset(&log, pubchan) - } - - case "until_next": - log.Info().Msg("Use until_next") - - var prog string = "default" - if d.State.Program_name != "" { - prog = d.State.Program_name - } - program, ok := d.Settings.Programs[prog] - if !ok { - return fmt.Errorf("program not found") - } - next, err := program.NextTime(d.State.Time) - if err!= nil { - return err - } - if time.Now().After(next) { - // force current program - // reset state - log.Info().Time("now", time.Now()).Time("next", next).Msg("until_next expired") - return d.setProgAndReset(&log, pubchan) - } - case "program": - log.Info().Msg("Use program mode") - if d.State.Setpoint == 0 { - //in case of new program mode - var prog string = "default" - if d.State.Program_name != "" { - prog = d.State.Program_name - } - value, err := d.setpointValueProg(prog) - if err != nil { - return err - } - d.State.Setpoint = value - d.PublishState(d.State, pubchan) - } - - default: - log.Info().Msg("Use default mode") - return d.setProgAndReset(&log, pubchan) - } - - if d.State.Setpoint != d.CurrentSetpoint { - log.Warn().Msg("Need setpoint update") - return d.publishSetpoint(d.State.Setpoint, pubchan) - } else { - log.Warn().Msg("No setpoint update") - } - - return nil +func (d Device) ListenTopic() (string, error) { + return d.Settings.TVR.FormatTopicState(d.Name) } -func (d *Device) setProgAndReset(logger *zerolog.Logger, pubchan chan Message) error { +func (d *Device) Program() (WeekProgram, error) { + // return current device program if specified or default one prog_name := "default" if d.State.Program_name != "" { prog_name = d.State.Program_name } - log := logger.With().Str("program", prog_name).Logger() - value, err := d.setpointValueProg(prog_name) - if err != nil { - return err - } - log = log.With().Int("futur_setpoint", value).Logger() - - if d.CurrentSetpoint != value { - log.Warn().Msg("publish setpoint update") - err = d.publishSetpoint(value, pubchan) - if err != nil { - return err - } - } else { - log.Warn().Msg("no setpoint update") + 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)) } - state := DeviceState{ - Setpoint: value, - Mode: "program", - Program_name: prog_name, - Time: time.Now(), - } + return program, nil +} - d.State = state +func (d *Device) ProgramName() (string) { + prog_name := "default" + if d.State.Program_name != "" { + prog_name = d.State.Program_name + } + return prog_name +} + +func (d *Device) SetState(state DeviceState, pubchan chan Message) error { payload, err := json.Marshal(state) if err != nil { @@ -169,10 +99,13 @@ func (d *Device) setProgAndReset(logger *zerolog.Logger, pubchan chan Message) e Payload: payload, Retain: true, } + + d.State = state + return nil } -func (d *Device) publishSetpoint(value int, pubchan chan Message) error { +func (d *Device) SetSetpoint(value int, pubchan chan Message) error { topic, err := d.Settings.TVR.FormatTopic(d.Name) if err != nil { return err @@ -191,22 +124,70 @@ func (d *Device) publishSetpoint(value int, pubchan chan Message) error { return nil } -func (d *Device) PublishState(state DeviceState, pubchan chan Message) error { - payload, err := json.Marshal(state) + +func (d *Device) CheckSetpoint(logger *zerolog.Logger, pubchan chan Message) error { + // Handle all the update setpoint logic + // push in pubChan a Message if setpoint need to be update + log := logger.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.Program_name). + Logger() + log.Info().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.setProgAndReset(&log, pubchan) + } + +} + +func (d *Device) setProgAndReset(log *zerolog.Logger, pubchan chan Message) error { + program, err := d.Program() if err != nil { return err } - pubchan <- Message{ - Topic: d.StateTopic(), - Payload: payload, - Retain: true, + + current_setpoint := program.Current() + + value, err := current_setpoint.Value(d.Settings.Presets) + if err != nil { + return err } - return nil + + 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) ListenTopic() (string, error) { - return d.Settings.TVR.FormatTopicState(d.Name) -} func (d *Device) onMessage(ctx context.Context, msg mqtt.Message) { log := zerolog.Ctx(ctx).With(). @@ -241,30 +222,83 @@ func (d *Device) onMessage(ctx context.Context, msg mqtt.Message) { } -func (d *Device) programSetpoint(prog_name string) (Setpoint, error) { - program, ok := d.Settings.Programs[prog_name] - if !ok { - return Setpoint{}, fmt.Errorf("device %s don't have %s program", d.Name, prog_name) + +func (d *Device) handle_always(log *zerolog.Logger, pubchan chan Message) error { + if d.State.Setpoint != d.CurrentSetpoint { + return d.SetSetpoint(d.State.Setpoint, pubchan) } - setpoint := program.Current() - return setpoint, nil + + return nil } -func (d *Device) setpointValue(setpoint Setpoint) (int, error) { - if len(d.Settings.Presets) < setpoint.Preset_id + 1 { - return 0, fmt.Errorf("Preset id %d didn't found", setpoint.Preset_id) +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) } - return d.Settings.Presets[setpoint.Preset_id].Value, nil + + if d.State.Setpoint != d.CurrentSetpoint { + log.Info().Msg("need setpoint update") + return d.SetSetpoint(d.State.Setpoint, pubchan) + } + + return nil } -func (d *Device) setpointValueProg(prog_name string) (int, error) { - setpoint, err := d.programSetpoint(prog_name) - if err != nil{ - return 0, err +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() + if err != nil { + return err } - val, err := d.setpointValue(setpoint) - if err != nil{ - return 0, err + + next, err := program.NextTime(d.State.Time) + if err!= nil { + return err } - return val, nil + + 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() + + 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") + } + + return nil } diff --git a/pkg/device/device_test.go b/pkg/device/device_test.go index 16f8fa1..2e21942 100644 --- a/pkg/device/device_test.go +++ b/pkg/device/device_test.go @@ -2,23 +2,183 @@ package device import ( "testing" + "time" + "fmt" + "reflect" + "github.com/rs/zerolog" + "io/ioutil" ) -var goodDefaultDevice = Device { +// monday +var test_time = time.Date(2022, time.October, 24, 0, 0, 0, 0, time.Local) + + + + +var test_presets = []Preset{ + {Label: "default", Value: 17, Color: "#012a36"}, + {Label: "normal", Value: 19, Color: "#b6244f"}, +} + +var test_setpoints = []Setpoint{ + {Start: 7*60, Preset_id: 1}, + {Start: 8*60, Preset_id: 0}, + {Start: 16*60, Preset_id: 1}, + {Start: 22*60, Preset_id: 0}, +} + +var test_weekprogram = WeekProgram{ + Monday: test_setpoints, + Thuesday: test_setpoints, + Wednesday: test_setpoints, + Thursday : test_setpoints, + Friday: test_setpoints, + Saturday: test_setpoints, + Sunday: test_setpoints, +} + +var test_programs = Programs{ + "default": test_weekprogram, +} + +var test_device = Device { Name: "valid", - Settings: DefaultDeviceSettings, + Settings: DeviceSettings{ + Programs: test_programs, + Presets: test_presets, + TVR: DefaultTVRSettings, + }, CurrentSetpoint: 0, - State: DeviceState{}, + State: DeviceState{ + Mode: "program", + Setpoint: 14, + Time: test_time, + Program_name: "default", + }, } func TestStateTopic(t *testing.T) { - topic := goodDefaultDevice.StateTopic() + topic := test_device.StateTopic() if topic != "heater/valid/state" { t.Errorf("Got %s; want heater/valid/state", topic) } } +func TestProgram(t *testing.T) { + //case 1: no program set in state return default + case1_device := test_device + case1_device.State.Program_name = "" + + //case 2: program set "confort" must return it + var test_confort_weekprogram = WeekProgram{ + Monday: test_setpoints, + Thuesday: test_setpoints, + Wednesday: test_setpoints, + Thursday : test_setpoints, + Friday: test_setpoints, + Saturday: test_setpoints, + Sunday: test_setpoints, + } + case2_device := test_device + case2_device.Settings.Programs = Programs{ + "default": test_weekprogram, + "confort": test_confort_weekprogram, + } + case2_device.State.Program_name = "confort" + + //case 3: program set "confort" but not exist + case3_device := test_device + case3_device.State.Program_name = "confort" + + var tests = []struct { + name string + device Device + result WeekProgram + err error + }{ + {"case 1 no program set use default", case1_device, DefaultWeekProgram, 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")}, + } + for _, tt := range tests { + testname := fmt.Sprintf("%s", tt.name ) + t.Run(testname, func(t *testing.T) { + prog, err := tt.device.Program() + if err != tt.err { + t.Errorf("got %s, want %s", err, tt.err) + } + if !reflect.DeepEqual(prog,tt.result) { + t.Errorf("got %v, want %v", prog, tt.result) + } + }) + } +} + func TestCheckSetpoint(t *testing.T) { + timeNow = func() time.Time { + return test_time + } + + device1 := test_device + device1.Name = "1" + device1.State = DeviceState{ + Mode: "program", + Setpoint: 0, + Time: test_time, + Program_name: "", + } + + device2 := test_device + device2.Name = "2" + device2.State.Program_name = "unknown" + + var tests = []struct{ + device Device + want []Message + err error + }{ + {device1, []Message{Message{ + Payload: []byte("{\"current_heating_setpoint\": 17}"), + Topic: "zigbee2mqtt/TVR/1/set", + Retain: false, + }}, nil} , + {device2, []Message{}, Error("device 2 don't have unknown program")}, + } + + for _, tt := range tests { + testname := fmt.Sprintf("%s", tt.device.Name ) + t.Run(testname, func(t *testing.T) { + + result := []Message{} + + rchan := make(chan Message, 10) + errchan := make(chan error, 1) + + go func(result chan Message, errchan chan error) { + logger := zerolog.New(ioutil.Discard).With().Timestamp().Logger() + errchan <- tt.device.CheckSetpoint(&logger, result) + close(rchan) + close(errchan) + }(rchan, errchan) + + for msg := range rchan { + result = append(result, msg) + } + + err, ok := <- errchan + if !ok { + t.Fatal(err) + } + + if err != tt.err { + t.Errorf("got %s, want %s", err, tt.err) + } + + if !reflect.DeepEqual(result,tt.want) { + t.Errorf("got %v, want %v", result, tt.want) + } + }) + } } diff --git a/pkg/device/settings.go b/pkg/device/settings.go index 18ecede..7270d20 100644 --- a/pkg/device/settings.go +++ b/pkg/device/settings.go @@ -75,13 +75,21 @@ type Setpoint struct { Preset_id int `json:"preset_id"` } +func (s Setpoint) Value(presets []Preset) (int, error) { + //TODO need test + if len(presets) < s.Preset_id + 1 { + return 0, Error(fmt.Sprintf("preset id %d not found", s.Preset_id)) + } + return presets[s.Preset_id].Value, nil +} type WeekProgram map[DayOfWeek][]Setpoint func (p WeekProgram) Current() Setpoint { + // TODO: need test // return current Setpoint - now := time.Now() + now := timeNow() weekday := weekday(now) daytime := daytime(now) setpoint := Setpoint{} @@ -236,10 +244,10 @@ var DefaultPrograms = Programs{ } var DefaultTVRSettings = TVRSettings { - Setpoint_topic : "zigbee2mqtt/TVR/{{.Device}}/set", - Setpoint_payload : "{\"current_heating_setpoint\": {{.Setpoint}}}", - Setpoint_state_topic : "zigbee2mqtt/TVR/{{.Device}}", - Setpoint_state_jp : "$.current_heating_setpoint", + Setpoint_topic : "zigbee2mqtt/TVR/{{.Device}}/set", + Setpoint_payload : "{\"current_heating_setpoint\": {{.Setpoint}}}", + Setpoint_state_topic : "zigbee2mqtt/TVR/{{.Device}}", + Setpoint_state_jp : "$.current_heating_setpoint", } var DefaultDeviceSettings = DeviceSettings{