GoSungrow/mmHa/struct.go
2022-12-14 17:50:18 +11:00

646 lines
15 KiB
Go

package mmHa
import (
"GoSungrow/iSolarCloud/AppService/getDeviceList"
"GoSungrow/iSolarCloud/api"
"GoSungrow/iSolarCloud/api/GoStruct/valueTypes"
"encoding/json"
"errors"
"fmt"
"github.com/MickMake/GoUnify/Only"
mqtt "github.com/eclipse/paho.mqtt.golang"
"net/url"
"time"
)
type Mqtt struct {
ClientId string `json:"client_id"`
Username string `json:"username"`
Password string `json:"password"`
Host string `json:"host"`
Port string `json:"port"`
Timeout time.Duration `json:"timeout"`
EntityPrefix string `json:"entity_prefix"`
url *url.URL
client mqtt.Client
pubClient mqtt.Client
clientOptions *mqtt.ClientOptions
LastRefresh time.Time `json:"-"`
SungrowDevices getDeviceList.Devices `json:"-"`
// SungrowDevices valueTypes.PsKeys `json:"-"`
// SungrowDevices valueTypes.PsIds `json:"-"`
// SungrowDevices getPsTreeMenu.ResultData `json:"-"`
SungrowPsIds map[valueTypes.PsId]bool
DeviceName string
MqttDevices map[string]Device
servicePrefix string
sensorPrefix string
lightPrefix string
switchPrefix string
binarySensorPrefix string
token mqtt.Token
firstRun bool
err error
}
func New(req Mqtt) *Mqtt {
var ret Mqtt
for range Only.Once {
ret.err = ret.setUrl(req)
if ret.err != nil {
break
}
ret.firstRun = true
ret.EntityPrefix = req.EntityPrefix
ret.servicePrefix = "homeassistant/sensor/" + req.ClientId
ret.sensorPrefix = "homeassistant/sensor/" + req.ClientId
ret.lightPrefix = "homeassistant/light/" + req.ClientId
ret.switchPrefix = "homeassistant/switch/" + req.ClientId
ret.binarySensorPrefix = "homeassistant/binary_sensor/" + req.ClientId
ret.MqttDevices = make(map[string]Device)
ret.SungrowPsIds = make(map[valueTypes.PsId]bool)
}
return &ret
}
func (m *Mqtt) IsFirstRun() bool {
return m.firstRun
}
func (m *Mqtt) IsNotFirstRun() bool {
return !m.firstRun
}
func (m *Mqtt) UnsetFirstRun() {
m.firstRun = false
}
func (m *Mqtt) GetError() error {
return m.err
}
func (m *Mqtt) IsError() bool {
if m.err != nil {
return true
}
return false
}
func (m *Mqtt) IsNewDay() bool {
var yes bool
for range Only.Once {
last := m.LastRefresh.Format("20060102")
now := time.Now().Format("20060102")
if last != now {
yes = true
break
}
}
return yes
}
func (m *Mqtt) setUrl(req Mqtt) error {
for range Only.Once {
// if req.Username == "" {
// m.err = errors.New("username empty")
// break
// }
m.Username = req.Username
// if req.Password == "" {
// m.err = errors.New("password empty")
// break
// }
m.Password = req.Password
if req.Host == "" {
m.err = errors.New("HASSIO mqtt host not defined")
break
}
m.Host = req.Host
if req.Port == "" {
req.Port = "1883"
}
m.Port = req.Port
u := fmt.Sprintf("tcp://%s:%s@%s:%s",
m.Username,
m.Password,
m.Host,
m.Port,
)
m.url, m.err = url.Parse(u)
}
return m.err
}
func (m *Mqtt) SetAuth(username string, password string) error {
for range Only.Once {
if username == "" {
m.err = errors.New("username empty")
break
}
m.Username = username
if password == "" {
m.err = errors.New("password empty")
break
}
m.Password = password
}
return m.err
}
func (m *Mqtt) Connect() error {
for range Only.Once {
m.err = m.createClientOptions()
if m.err != nil {
break
}
m.client = mqtt.NewClient(m.clientOptions)
token := m.client.Connect()
for !token.WaitTimeout(3 * time.Second) {
}
if m.err = token.Error(); m.err != nil {
break
}
if m.ClientId == "" {
m.ClientId = "GoSungrow"
}
device := Config {
Entry: m.servicePrefix,
Name: m.ClientId,
UniqueId: m.ClientId, // + "_Service",
StateTopic: "~/state",
DeviceConfig: DeviceConfig {
Identifiers: []string{"GoSungrow"},
SwVersion: "GoSungrow https://github.com/MickMake/GoSungrow",
Name: m.ClientId + " Service",
Manufacturer: "MickMake",
Model: "SunGrow",
},
}
m.err = m.Publish(JoinStringsForTopic(m.servicePrefix, "config"), 0, true, device.Json())
if m.err != nil {
break
}
m.err = m.Publish(JoinStringsForTopic(m.servicePrefix, "state"), 0, true, "ON")
if m.err != nil {
break
}
}
return m.err
}
func (m *Mqtt) Disconnect() error {
for range Only.Once {
m.client.Disconnect(250)
time.Sleep(1 * time.Second)
}
return m.err
}
func (m *Mqtt) createClientOptions() error {
for range Only.Once {
m.clientOptions = mqtt.NewClientOptions()
m.clientOptions.AddBroker(fmt.Sprintf("tcp://%s", m.url.Host))
m.clientOptions.SetUsername(m.url.User.Username())
password, _ := m.url.User.Password()
m.clientOptions.SetPassword(password)
m.clientOptions.SetClientID(m.ClientId)
m.clientOptions.WillTopic = JoinStringsForTopic(m.servicePrefix, "state")
m.clientOptions.WillPayload = []byte("OFF")
m.clientOptions.WillQos = 0
m.clientOptions.WillRetained = true
m.clientOptions.WillEnabled = true
}
return m.err
}
func (m *Mqtt) Subscribe(topic string) error {
for range Only.Once {
t := m.client.Subscribe(topic, 0, func(client mqtt.Client, msg mqtt.Message) {
fmt.Printf("* [%s] %s\n", msg.Topic(), string(msg.Payload()))
})
if !t.WaitTimeout(m.Timeout) {
m.err = t.Error()
// m.err = errors.New("mqtt subscribe timeout")
}
}
return m.err
}
func (m *Mqtt) Publish(topic string, qos byte, retained bool, payload interface{}) error {
for range Only.Once {
t := m.client.Publish(topic, qos, retained, payload)
if !t.WaitTimeout(m.Timeout) {
m.err = t.Error()
// m.err = errors.New("mqtt publish timeout")
}
}
return m.err
}
func (m *Mqtt) PublishState(Type string, subtopic string, payload interface{}) error {
for range Only.Once {
// topic = JoinStringsForId(m.EntityPrefix, m.Device.Name, topic)
// topic = JoinStringsForTopic(m.sensorPrefix, topic, "state")
// st := JoinStringsForTopic(m.sensorPrefix, JoinStringsForId(m.EntityPrefix, m.Device.FullName, strings.ReplaceAll(subName, "/", ".")), "state")
topic := ""
switch Type {
case "sensor":
topic = JoinStringsForTopic(m.sensorPrefix, subtopic, "state")
case "binary_sensor":
topic = JoinStringsForTopic(m.binarySensorPrefix, subtopic, "state")
case "lights":
topic = JoinStringsForTopic(m.lightPrefix, subtopic, "state")
case "switch":
topic = JoinStringsForTopic(m.switchPrefix, subtopic, "state")
default:
topic = JoinStringsForTopic(m.sensorPrefix, subtopic, "state")
}
t := m.client.Publish(topic, 0, true, payload)
if !t.WaitTimeout(m.Timeout) {
m.err = t.Error()
}
}
return m.err
}
func (m *Mqtt) PublishValue(Type string, subtopic string, value string) error {
for range Only.Once {
topic := ""
switch Type {
case "sensor":
topic = JoinStringsForTopic(m.sensorPrefix, subtopic, "state")
// state := MqttState {
// LastReset: "", // m.GetLastReset(point.PointId),
// Value: value,
// }
// value = state.Json()
case "binary_sensor":
topic = JoinStringsForTopic(m.binarySensorPrefix, subtopic, "state")
// state := MqttState {
// LastReset: "", // m.GetLastReset(point.PointId),
// Value: value,
// }
// value = state.Json()
case "lights":
topic = JoinStringsForTopic(m.lightPrefix, subtopic, "state")
// state := MqttState {
// LastReset: "", // m.GetLastReset(point.PointId),
// Value: value,
// }
// value = state.Json()
case "switch":
topic = JoinStringsForTopic(m.switchPrefix, subtopic, "state")
// state := MqttState {
// LastReset: "", // m.GetLastReset(point.PointId),
// Value: value,
// }
// value = state.Json()
default:
topic = JoinStringsForTopic(m.sensorPrefix, subtopic, "state")
}
// t = JoinStringsForId(m.EntityPrefix, m.Device.Name, t)
// st := JoinStringsForTopic(m.sensorPrefix, JoinStringsForId(m.EntityPrefix, m.Device.FullName, strings.ReplaceAll(subName, "/", ".")), "state")
// payload := MqttState {
// LastReset: "", // m.GetLastReset(point.PointId),
// Value: value,
// }
// m.client.Publish(JoinStringsForTopic(m.sensorPrefix, t, "state"), 0, true, payload.Json())
t := m.client.Publish(topic, 0, true, value)
if !t.WaitTimeout(m.Timeout) {
m.err = t.Error()
}
}
return m.err
}
func (m *Mqtt) SetDeviceConfig(swname string, parentId string, id string, name string, model string, vendor string, area string) error {
for range Only.Once {
// id = JoinStringsForId(m.EntityPrefix, id)
c := [][]string{
{swname, JoinStringsForId(m.EntityPrefix, parentId)},
{JoinStringsForId(m.EntityPrefix, parentId), JoinStringsForId(m.EntityPrefix, id)},
}
if swname == parentId {
c = [][]string{
{parentId, JoinStringsForId(m.EntityPrefix, id)},
}
}
m.MqttDevices[id] = Device {
Connections: c,
Identifiers: []string{JoinStringsForId(m.EntityPrefix, id)},
Manufacturer: vendor,
Model: model,
Name: name,
SwVersion: swname + " https://github.com/MickMake/" + swname,
ViaDevice: swname,
SuggestedArea: area,
}
}
return m.err
}
func (m *Mqtt) GetLastReset(pointType string) string {
var ret string
for range Only.Once {
pt := api.GetDevicePoint(pointType)
if !pt.Valid {
break
}
if pt.UpdateFreq == "" {
break
}
ret = pt.WhenReset()
}
return ret
}
type MqttState struct {
LastReset string `json:"last_reset,omitempty"`
Value string `json:"value"`
}
func (mq *MqttState) Json() string {
var ret string
for range Only.Once {
j, err := json.Marshal(*mq)
if err != nil {
ret = fmt.Sprintf("{ \"error\": \"%s\"", err)
break
}
ret = string(j)
}
return ret
}
type Availability struct {
PayloadAvailable string `json:"payload_available,omitempty" required:"false"`
PayloadNotAvailable string `json:"payload_not_available,omitempty" required:"false"`
Topic string `json:"topic,omitempty" required:"true"`
ValueTemplate string `json:"value_template,omitempty" required:"false"`
}
type SensorState string
type EntityConfig struct {
// Type string
Name string
SubName string
ParentId string
ParentName string
UniqueId string
FullId string
Units string
ValueName string
DeviceClass string
StateClass string
Icon string
Value string
ValueTemplate string
UpdateFreq string
LastReset string
LastResetValueTemplate string
haType string
}
func (config *EntityConfig) IsSensor() bool {
var ok bool
for range Only.Once {
if config.IsBinarySensor() {
break
}
if config.IsSwitch() {
break
}
if config.IsLight() {
break
}
// if config.Value != "" {
// ok = true
// break
// }
// if config.Units == "" {
// break
// }
ok = true
}
return ok
}
func (config *EntityConfig) IsBinarySensor() bool {
var ok bool
for range Only.Once {
if config.Units == LabelBinarySensor {
ok = true
break
}
if config.Units == "Bool" {
ok = true
break
}
}
return ok
}
func (config *EntityConfig) IsSwitch() bool {
var ok bool
for range Only.Once {
if config.Units == LabelSwitch {
ok = true
break
}
}
return ok
}
func (config *EntityConfig) IsLight() bool {
var ok bool
for range Only.Once {
if config.Units == LabelLight {
ok = true
break
}
}
return ok
}
func (config *EntityConfig) FixConfig() {
for range Only.Once {
// mdi:power-socket-au
// mdi:solar-power
// mdi:home-lightning-bolt-outline
// mdi:transmission-tower
// mdi:transmission-tower-export
// mdi:transmission-tower-import
// mdi:transmission-tower-off
// mdi:home-battery-outline
// mdi:lightning-bolt
// mdi:check-circle-outline | mdi:arrow-right-bold
switch config.Units {
case "Bool":
fallthrough
case LabelBinarySensor:
config.DeviceClass = SetDefault(config.DeviceClass, "power")
config.Icon = SetDefault(config.Icon, "mdi:check-circle-outline")
config.ValueTemplate = SetDefault(config.ValueTemplate, "{{ value_json.value }}")
case "MW":
fallthrough
case "kW":
fallthrough
case "W":
config.DeviceClass = SetDefault(config.DeviceClass, "power")
config.Icon = SetDefault(config.Icon, "mdi:lightning-bolt")
config.ValueTemplate = SetDefault(config.ValueTemplate, "{{ value_json.value | float }}")
// config.ValueTemplate = SetDefault(config.ValueTemplate, fmt.Sprintf("{{ value_json.%s | float }}", config.ValueName))
// - Used with merged values.
case "MWh":
fallthrough
case "kWh":
fallthrough
case "Wh":
config.DeviceClass = SetDefault(config.DeviceClass, "energy")
config.Icon = SetDefault(config.Icon, "mdi:lightning-bolt")
config.ValueTemplate = SetDefault(config.ValueTemplate, "{{ value_json.value | float }}")
case "kvar":
config.DeviceClass = SetDefault(config.DeviceClass, "reactive_power")
config.Icon = SetDefault(config.Icon, "mdi:lightning-bolt")
config.ValueTemplate = SetDefault(config.ValueTemplate, "{{ value_json.value | float }}")
case "Hz":
config.DeviceClass = SetDefault(config.DeviceClass, "frequency")
config.Icon = SetDefault(config.Icon, "mdi:sine-wave")
config.ValueTemplate = SetDefault(config.ValueTemplate, "{{ value_json.value | float }}")
case "V":
config.DeviceClass = SetDefault(config.DeviceClass, "voltage")
config.Icon = SetDefault(config.Icon, "mdi:current-dc")
config.ValueTemplate = SetDefault(config.ValueTemplate, "{{ value_json.value | float }}")
case "A":
config.DeviceClass = SetDefault(config.DeviceClass, "current")
config.Icon = SetDefault(config.Icon, "mdi:current-ac")
config.ValueTemplate = SetDefault(config.ValueTemplate, "{{ value_json.value | float }}")
case "°F":
fallthrough
case "F":
fallthrough
case "℉":
fallthrough
case "°C":
fallthrough
case "C":
fallthrough
case "℃":
config.DeviceClass = SetDefault(config.DeviceClass, "temperature")
config.Units = "°C"
config.Icon = SetDefault(config.Icon, "mdi:thermometer")
config.ValueTemplate = SetDefault(config.ValueTemplate, "{{ value_json.value | float }}")
case "%":
config.DeviceClass = SetDefault(config.DeviceClass, "battery")
config.Icon = SetDefault(config.Icon, "mdi:home-battery-outline")
config.ValueTemplate = SetDefault(config.ValueTemplate, "{{ value_json.value | float }}")
default:
config.DeviceClass = SetDefault(config.DeviceClass, "")
config.Icon = SetDefault(config.Icon, "")
config.ValueTemplate = SetDefault(config.ValueTemplate, "{{ value_json.value }}")
}
if config.LastReset != "" {
break
}
// pt := api.GetDevicePoint(config.FullId)
// if !pt.Valid {
// break
// }
if config.StateClass == "instant" {
config.StateClass = "measurement"
break
}
if config.StateClass == "" {
config.StateClass = "measurement"
break
}
// config.LastReset = pt.WhenReset()
config.LastResetValueTemplate = SetDefault(config.LastResetValueTemplate, "{{ value_json.last_reset | as_datetime() }}")
// config.LastResetValueTemplate = SetDefault(config.LastResetValueTemplate, "{{ value_json.last_reset | int | timestamp_local | as_datetime }}")
if config.LastReset == "" {
config.StateClass = "measurement"
break
}
config.StateClass = "total"
}
}
func SetDefault(value string, def string) string {
if value == "" {
value = def
}
return value
}