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 }