307 lines
6.3 KiB
Go
307 lines
6.3 KiB
Go
package device
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/ohler55/ojg/jp"
|
|
)
|
|
|
|
type DayOfWeek int
|
|
|
|
func (d DayOfWeek) Next() DayOfWeek {
|
|
if d == Sunday {
|
|
return Monday
|
|
}
|
|
return d + 1
|
|
}
|
|
|
|
func (d DayOfWeek) Previous() DayOfWeek {
|
|
if d == Monday {
|
|
return Sunday
|
|
}
|
|
return d - 1
|
|
}
|
|
|
|
func (d DayOfWeek) DaysBetween(n DayOfWeek) int {
|
|
var between int
|
|
if n < d {
|
|
between = 7 - int(d-n)
|
|
} else {
|
|
between = int(n - d)
|
|
}
|
|
return between
|
|
}
|
|
|
|
const (
|
|
Monday DayOfWeek = iota
|
|
Thuesday
|
|
Wednesday
|
|
Thursday
|
|
Friday
|
|
Saturday
|
|
Sunday
|
|
)
|
|
|
|
func WeekDayEnToFr(weekday time.Weekday) DayOfWeek {
|
|
// translate weekday to french week, start by Monday
|
|
return map[time.Weekday]DayOfWeek{
|
|
time.Monday: Monday,
|
|
time.Tuesday: Thuesday,
|
|
time.Wednesday: Wednesday,
|
|
time.Thursday: Thursday,
|
|
time.Friday: Friday,
|
|
time.Saturday: Saturday,
|
|
time.Sunday: Sunday,
|
|
}[weekday]
|
|
}
|
|
|
|
func daytime(t time.Time) int {
|
|
return t.Hour()*60 + t.Minute()
|
|
}
|
|
|
|
func weekday(t time.Time) DayOfWeek {
|
|
return WeekDayEnToFr(t.Weekday())
|
|
}
|
|
|
|
type Message struct {
|
|
Payload []byte
|
|
Topic string
|
|
Retain bool
|
|
}
|
|
|
|
type Setpoint struct {
|
|
Start int `json:"start"`
|
|
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 := timeNow()
|
|
weekday := weekday(now)
|
|
daytime := daytime(now)
|
|
setpoint := Setpoint{}
|
|
for _, sp := range p[weekday] {
|
|
if daytime < sp.Start {
|
|
break
|
|
}
|
|
setpoint = sp
|
|
}
|
|
return setpoint
|
|
}
|
|
|
|
func (p WeekProgram) NextTime(t time.Time) (time.Time, error) {
|
|
// return next program change
|
|
|
|
weekday := weekday(t)
|
|
daytime := daytime(t)
|
|
|
|
// Recursive func to find setpoint on weekday
|
|
get := func(weekday DayOfWeek, daytime int) (Setpoint, bool) {
|
|
for _, sp := range p[weekday] {
|
|
if daytime <= sp.Start {
|
|
return sp, true
|
|
}
|
|
}
|
|
return Setpoint{}, false
|
|
}
|
|
|
|
startweekday := weekday
|
|
for {
|
|
if setpoint, ok := get(weekday, daytime); ok {
|
|
next := time.Date(t.Year(),
|
|
t.Month(),
|
|
t.Day()+startweekday.DaysBetween(weekday),
|
|
0, 0, 0, 0,
|
|
time.Local)
|
|
|
|
next = next.Add(
|
|
time.Duration((setpoint.Start) * int(time.Minute)))
|
|
return next, nil
|
|
}
|
|
|
|
weekday = weekday.Next()
|
|
daytime = 0
|
|
if weekday == startweekday {
|
|
return time.Time{}, Error("shouldn't happen no setpoint found over the week")
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
type Programs map[string]WeekProgram
|
|
|
|
type Preset struct {
|
|
Label string `json:"label"`
|
|
Value int `json:"value"`
|
|
Color string `json:"color"`
|
|
}
|
|
|
|
type TVRSettings struct {
|
|
Setpoint_topic string `json:"setpoint_topic"`
|
|
Setpoint_payload string `json:"setpoint_payload"`
|
|
Setpoint_state_topic string `json:"setpoint_state_topic"`
|
|
Setpoint_state_jp string `json:"setpoint_state_jp"`
|
|
StateLocalTemperatureJP string `json:"state_local_temperature_jp"`
|
|
StateValveJP string `json:"state_valve_jp"`
|
|
}
|
|
|
|
func (s TVRSettings) ParseSetpoint(obj interface{}) (int, error) {
|
|
jp, err := jp.ParseString(s.Setpoint_state_jp)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
r := jp.First(obj)
|
|
|
|
if v, ok := r.(int64); ok {
|
|
return int(v), nil
|
|
} else {
|
|
return 0, Error("Unexpected type")
|
|
}
|
|
}
|
|
|
|
func (s TVRSettings) ParseLocalTemperature(obj interface{}) (int, error) {
|
|
jp, err := jp.ParseString(s.StateLocalTemperatureJP)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
r := jp.First(obj)
|
|
|
|
if v, ok := r.(int64); ok {
|
|
return int(v), nil
|
|
} else {
|
|
return 0, Error("Unexpected type")
|
|
}
|
|
}
|
|
|
|
func (s TVRSettings) ParseValve(obj interface{}) (string, error) {
|
|
jp, err := jp.ParseString(s.StateValveJP)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
r := jp.First(obj)
|
|
|
|
if v, ok := r.(string); ok {
|
|
return string(v), nil
|
|
} else {
|
|
return "", Error("Unexpected type")
|
|
}
|
|
}
|
|
|
|
func (s TVRSettings) FormatTopicState(device_name string) (string, error) {
|
|
type Variable struct {
|
|
Device string
|
|
}
|
|
variables := Variable{device_name}
|
|
t, err := template.New("topic").Parse(s.Setpoint_state_topic)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
buf := new(bytes.Buffer)
|
|
err = t.Execute(buf, variables)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return buf.String(), nil
|
|
}
|
|
|
|
func (s TVRSettings) FormatTopic(device_name string) (string, error) {
|
|
type Variable struct {
|
|
Device string
|
|
}
|
|
variables := Variable{device_name}
|
|
t, err := template.New("topic").Parse(s.Setpoint_topic)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
buf := new(bytes.Buffer)
|
|
err = t.Execute(buf, variables)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return buf.String(), nil
|
|
}
|
|
|
|
func (s TVRSettings) FormatPayload(setpoint int) (string, error) {
|
|
type Variable struct {
|
|
Setpoint int
|
|
}
|
|
variables := Variable{setpoint}
|
|
t, err := template.New("payload").Parse(s.Setpoint_payload)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
buf := new(bytes.Buffer)
|
|
err = t.Execute(buf, variables)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return buf.String(), nil
|
|
}
|
|
|
|
type DeviceSettings struct {
|
|
Programs Programs `json:"programs"`
|
|
Presets []Preset `json:"presets"`
|
|
TVR TVRSettings `json:"TVR"`
|
|
}
|
|
|
|
//////////////////////////////////////////////
|
|
// Defaults
|
|
|
|
var DefaultPresets = []Preset{
|
|
{Label: "default", Value: 17, Color: "#012a36"},
|
|
{Label: "normal", Value: 19, Color: "#b6244f"},
|
|
}
|
|
|
|
var defaultSetpoints = []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 DefaultWeekProgram = WeekProgram{
|
|
Monday: defaultSetpoints,
|
|
Thuesday: defaultSetpoints,
|
|
Wednesday: defaultSetpoints,
|
|
Thursday: defaultSetpoints,
|
|
Friday: defaultSetpoints,
|
|
Saturday: defaultSetpoints,
|
|
Sunday: defaultSetpoints,
|
|
}
|
|
|
|
var DefaultPrograms = Programs{
|
|
"default": DefaultWeekProgram,
|
|
}
|
|
|
|
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",
|
|
StateLocalTemperatureJP: "$.local_temperature",
|
|
StateValveJP: "$.valve_state",
|
|
}
|
|
|
|
var DefaultDeviceSettings = DeviceSettings{
|
|
Programs: DefaultPrograms,
|
|
Presets: DefaultPresets,
|
|
TVR: DefaultTVRSettings,
|
|
}
|