|
| 1 | +/* |
| 2 | +Copyright © 2021 Ben Buxton <bbuxton@gmail.com> |
| 3 | +
|
| 4 | +This program is free software: you can redistribute it and/or modify |
| 5 | +it under the terms of the GNU General Public License as published by |
| 6 | +the Free Software Foundation, either version 3 of the License, or |
| 7 | +(at your option) any later version. |
| 8 | +
|
| 9 | +This program is distributed in the hope that it will be useful, |
| 10 | +but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | +GNU General Public License for more details. |
| 13 | +
|
| 14 | +You should have received a copy of the GNU General Public License |
| 15 | +along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 16 | +*/ |
| 17 | +package cmd |
| 18 | + |
| 19 | +import ( |
| 20 | + "encoding/hex" |
| 21 | + "fmt" |
| 22 | + "github.com/buxtronix/phev2mqtt/client" |
| 23 | + "github.com/buxtronix/phev2mqtt/protocol" |
| 24 | + "github.com/spf13/cobra" |
| 25 | + "strings" |
| 26 | + "time" |
| 27 | + |
| 28 | + mqtt "github.com/eclipse/paho.mqtt.golang" |
| 29 | + log "github.com/sirupsen/logrus" |
| 30 | +) |
| 31 | + |
| 32 | +// mqttCmd represents the mqtt command |
| 33 | +var mqttCmd = &cobra.Command{ |
| 34 | + Use: "mqtt", |
| 35 | + Short: "Start an MQTT bridge.", |
| 36 | + Long: `A longer description that spans multiple lines and likely contains examples |
| 37 | +and usage of using your command. For example: |
| 38 | +
|
| 39 | +Cobra is a CLI library for Go that empowers applications. |
| 40 | +This application is a tool to generate the needed files |
| 41 | +to quickly create a Cobra application.`, |
| 42 | + RunE: func(cmd *cobra.Command, args []string) error { |
| 43 | + mc := &mqttClient{} |
| 44 | + return mc.Run(cmd, args) |
| 45 | + }, |
| 46 | +} |
| 47 | + |
| 48 | +type mqttClient struct { |
| 49 | + client mqtt.Client |
| 50 | + options *mqtt.ClientOptions |
| 51 | + mqttData map[string]string |
| 52 | + updateInterval time.Duration |
| 53 | + |
| 54 | + phev *client.Client |
| 55 | + |
| 56 | + prefix string |
| 57 | +} |
| 58 | + |
| 59 | +func (m *mqttClient) topic(topic string) string { |
| 60 | + return fmt.Sprintf("%s%s", m.prefix, topic) |
| 61 | +} |
| 62 | + |
| 63 | +func (m *mqttClient) Run(cmd *cobra.Command, args []string) error { |
| 64 | + var err error |
| 65 | + |
| 66 | + mqttServer, _ := cmd.Flags().GetString("mqtt_server") |
| 67 | + mqttUsername, _ := cmd.Flags().GetString("mqtt_username") |
| 68 | + mqttPassword, _ := cmd.Flags().GetString("mqtt_password") |
| 69 | + m.prefix, _ = cmd.Flags().GetString("mqtt_topic_prefix") |
| 70 | + |
| 71 | + m.updateInterval, err = cmd.Flags().GetDuration("update_interval") |
| 72 | + if err != nil { |
| 73 | + return err |
| 74 | + } |
| 75 | + |
| 76 | + m.options = mqtt.NewClientOptions(). |
| 77 | + AddBroker(mqttServer). |
| 78 | + SetClientID("phev2mqtt"). |
| 79 | + SetUsername(mqttUsername). |
| 80 | + SetPassword(mqttPassword). |
| 81 | + SetAutoReconnect(true). |
| 82 | + SetDefaultPublishHandler(m.handleIncomingMqtt) |
| 83 | + |
| 84 | + m.client = mqtt.NewClient(m.options) |
| 85 | + if token := m.client.Connect(); token.Wait() && token.Error() != nil { |
| 86 | + return token.Error() |
| 87 | + } |
| 88 | + |
| 89 | + if token := m.client.Subscribe(m.topic("/set/#"), 0, nil); token.Wait() && token.Error() != nil { |
| 90 | + return token.Error() |
| 91 | + } |
| 92 | + |
| 93 | + m.mqttData = map[string]string{} |
| 94 | + |
| 95 | + for { |
| 96 | + if err := m.handlePhev(cmd); err != nil { |
| 97 | + log.Error(err) |
| 98 | + } |
| 99 | + m.publish("/available", "offline") |
| 100 | + time.Sleep(15 * time.Second) |
| 101 | + } |
| 102 | +} |
| 103 | + |
| 104 | +func (m *mqttClient) publish(topic, payload string) { |
| 105 | + if cache := m.mqttData[topic]; cache == payload { |
| 106 | + // Only publish new data. |
| 107 | + return |
| 108 | + } |
| 109 | + m.client.Publish(m.topic(topic), 0, false, payload) |
| 110 | + m.mqttData[topic] = payload |
| 111 | +} |
| 112 | + |
| 113 | +func (m *mqttClient) handleIncomingMqtt(client mqtt.Client, msg mqtt.Message) { |
| 114 | + log.Infof("Topic: %s", msg.Topic()) |
| 115 | + log.Infof("Payload: %s", msg.Payload()) |
| 116 | + |
| 117 | + topicParts := strings.Split(msg.Topic(), "/") |
| 118 | + if strings.HasPrefix(msg.Topic(), m.topic("/set/register/")) { |
| 119 | + if len(topicParts) != 4 { |
| 120 | + log.Infof("Bad topic format [%s]", msg.Topic()) |
| 121 | + return |
| 122 | + } |
| 123 | + register, err := hex.DecodeString(topicParts[3]) |
| 124 | + if err != nil { |
| 125 | + log.Infof("Bad register in topic [%s]: %v", msg.Topic(), err) |
| 126 | + return |
| 127 | + } |
| 128 | + data, err := hex.DecodeString(string(msg.Payload())) |
| 129 | + if err != nil { |
| 130 | + log.Infof("Bad payload [%s]: %v", msg.Payload(), err) |
| 131 | + return |
| 132 | + } |
| 133 | + if err := m.phev.SetRegister(register[0], data); err != nil { |
| 134 | + log.Infof("Error setting register %02x: %v", register[0], err) |
| 135 | + return |
| 136 | + } |
| 137 | + } else if msg.Topic() == m.topic("/set/parkinglights") { |
| 138 | + switch payload := string(msg.Payload()); payload { |
| 139 | + case "on": |
| 140 | + if err := m.phev.SetRegister(0xb, []byte{0x1}); err != nil { |
| 141 | + log.Infof("Error setting register 0xb: %v", err) |
| 142 | + return |
| 143 | + } |
| 144 | + case "off": |
| 145 | + if err := m.phev.SetRegister(0xb, []byte{0x2}); err != nil { |
| 146 | + log.Infof("Error setting register 0xb: %v", err) |
| 147 | + return |
| 148 | + } |
| 149 | + default: |
| 150 | + log.Errorf("Unsupported payload for %s: %s", msg.Topic(), payload) |
| 151 | + } |
| 152 | + } else if msg.Topic() == m.topic("/set/headlights") { |
| 153 | + switch payload := string(msg.Payload()); payload { |
| 154 | + case "on": |
| 155 | + if err := m.phev.SetRegister(0xa, []byte{0x1}); err != nil { |
| 156 | + log.Infof("Error setting register 0xb: %v", err) |
| 157 | + return |
| 158 | + } |
| 159 | + case "off": |
| 160 | + if err := m.phev.SetRegister(0xa, []byte{0x2}); err != nil { |
| 161 | + log.Infof("Error setting register 0xb: %v", err) |
| 162 | + return |
| 163 | + } |
| 164 | + default: |
| 165 | + log.Errorf("Unsupported payload for %s: %s", msg.Topic(), payload) |
| 166 | + } |
| 167 | + } else if msg.Topic() == m.topic("/set/cancelchargetimer") { |
| 168 | + if err := m.phev.SetRegister(0x17, []byte{0x1}); err != nil { |
| 169 | + log.Infof("Error setting register 0x17: %v", err) |
| 170 | + return |
| 171 | + } |
| 172 | + if err := m.phev.SetRegister(0x17, []byte{0x11}); err != nil { |
| 173 | + log.Infof("Error setting register 0x17: %v", err) |
| 174 | + return |
| 175 | + } |
| 176 | + } else { |
| 177 | + log.Errorf("Unknown topic from mqtt: %s", msg.Topic()) |
| 178 | + } |
| 179 | +} |
| 180 | + |
| 181 | +func (m *mqttClient) handlePhev(cmd *cobra.Command) error { |
| 182 | + var err error |
| 183 | + address, _ := cmd.Flags().GetString("address") |
| 184 | + m.phev, err = client.New(client.AddressOption(address)) |
| 185 | + if err != nil { |
| 186 | + return err |
| 187 | + } |
| 188 | + |
| 189 | + if err := m.phev.Connect(); err != nil { |
| 190 | + return err |
| 191 | + } |
| 192 | + |
| 193 | + if err := m.phev.Start(); err != nil { |
| 194 | + return err |
| 195 | + } |
| 196 | + m.publish("/available", "online") |
| 197 | + |
| 198 | + var encodingErrorCount = 0 |
| 199 | + var lastEncodingError time.Time |
| 200 | + |
| 201 | + updaterTicker := time.NewTicker(m.updateInterval) |
| 202 | + for { |
| 203 | + select { |
| 204 | + case <-updaterTicker.C: |
| 205 | + m.phev.SetRegister(0x6, []byte{0x3}) |
| 206 | + case msg, ok := <-m.phev.Recv: |
| 207 | + if !ok { |
| 208 | + log.Infof("Connection closed.") |
| 209 | + updaterTicker.Stop() |
| 210 | + return fmt.Errorf("Connection closed.") |
| 211 | + } |
| 212 | + switch msg.Type { |
| 213 | + case protocol.CmdInBadEncoding: |
| 214 | + if time.Now().Sub(lastEncodingError) > 15*time.Second { |
| 215 | + encodingErrorCount = 0 |
| 216 | + } |
| 217 | + if encodingErrorCount > 50 { |
| 218 | + m.phev.Close() |
| 219 | + updaterTicker.Stop() |
| 220 | + return fmt.Errorf("Disconnecting due to too many errors") |
| 221 | + } |
| 222 | + encodingErrorCount += 1 |
| 223 | + lastEncodingError = time.Now() |
| 224 | + case protocol.CmdInResp: |
| 225 | + if msg.Ack != protocol.Request { |
| 226 | + break |
| 227 | + } |
| 228 | + m.publishRegister(msg) |
| 229 | + m.phev.Send <- &protocol.PhevMessage{ |
| 230 | + Type: protocol.CmdOutSend, |
| 231 | + Register: msg.Register, |
| 232 | + Ack: protocol.Ack, |
| 233 | + Xor: msg.Xor, |
| 234 | + Data: []byte{0x0}, |
| 235 | + } |
| 236 | + } |
| 237 | + } |
| 238 | + } |
| 239 | +} |
| 240 | + |
| 241 | +var boolOnOff = map[bool]string{ |
| 242 | + false: "off", |
| 243 | + true: "on", |
| 244 | +} |
| 245 | + |
| 246 | +func (m *mqttClient) publishRegister(msg *protocol.PhevMessage) { |
| 247 | + dataStr := hex.EncodeToString(msg.Data) |
| 248 | + m.publish(fmt.Sprintf("/register/%02x", msg.Register), dataStr) |
| 249 | + switch reg := msg.Reg.(type) { |
| 250 | + case *protocol.RegisterVIN: |
| 251 | + m.publish("/vin", reg.VIN) |
| 252 | + m.publish("/registrations", fmt.Sprintf("%d", reg.Registrations)) |
| 253 | + case *protocol.RegisterECUVersion: |
| 254 | + m.publish("/ecuversion", reg.Version) |
| 255 | + case *protocol.RegisterACMode: |
| 256 | + m.publish("/ac/mode", reg.Mode) |
| 257 | + case *protocol.RegisterACOperStatus: |
| 258 | + m.publish("/ac/status", boolOnOff[reg.Operating]) |
| 259 | + case *protocol.RegisterChargeStatus: |
| 260 | + m.publish("/charge/charging", boolOnOff[reg.Charging]) |
| 261 | + m.publish("/charge/remaining", fmt.Sprintf("%d", reg.Remaining)) |
| 262 | + case *protocol.RegisterDoorStatus: |
| 263 | + m.publish("/door/locked", boolOnOff[reg.Locked]) |
| 264 | + case *protocol.RegisterBatteryLevel: |
| 265 | + m.publish("/battery/level", fmt.Sprintf("%d", reg.Level)) |
| 266 | + } |
| 267 | +} |
| 268 | + |
| 269 | +func init() { |
| 270 | + clientCmd.AddCommand(mqttCmd) |
| 271 | + |
| 272 | + // Here you will define your flags and configuration settings. |
| 273 | + |
| 274 | + // Cobra supports Persistent Flags which will work for this command |
| 275 | + // and all subcommands, e.g.: |
| 276 | + // mqttCmd.PersistentFlags().String("foo", "", "A help for foo") |
| 277 | + |
| 278 | + // Cobra supports local flags which will only run when this command |
| 279 | + // is called directly, e.g.: |
| 280 | + // mqttCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") |
| 281 | + mqttCmd.Flags().String("mqtt_server", "tcp://127.0.0.1:1883", "Address of MQTT server") |
| 282 | + mqttCmd.Flags().String("mqtt_username", "", "Username to login to MQTT server") |
| 283 | + mqttCmd.Flags().String("mqtt_password", "", "Password to login to MQTT server") |
| 284 | + mqttCmd.Flags().String("mqtt_topic_prefix", "phev", "Prefix for MQTT topics") |
| 285 | + mqttCmd.Flags().Duration("update_interval", 5*time.Minute, "How often to request force updates") |
| 286 | +} |
0 commit comments