├── Dockerfile ├── Makefile ├── apache-license-2.0.md ├── config.go ├── config.yaml ├── coulombmeter.go ├── docs └── TF03K communication specification.pdf ├── go.mod ├── go.sum ├── images ├── ha_integration.png └── tf03k.png ├── log.go ├── main.go ├── mqtt.go ├── mqtt_register.go └── readme.md /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.15.6-buster as builder 2 | WORKDIR /go/src/github.com/edillmann/tf03mqtt 3 | COPY *.mod /go/src/github.com/edillmann/tf03mqtt/ 4 | RUN go mod download 5 | COPY *.go /go/src/github.com/edillmann/tf03mqtt/ 6 | RUN go build 7 | 8 | FROM debian:buster 9 | WORKDIR /root/ 10 | COPY --from=builder /go/src/github.com/edillmann/tf03mqtt /root/ 11 | COPY config.yaml . 12 | CMD ["/root/tf03mqtt", "-config=/root/config.yaml" , "-loglevel=debug" ] 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | docker: 3 | docker build --network=host --tag tf03mqtt:latest . 4 | 5 | run: 6 | docker run --name tf03mqtt_agent -d --restart=always --privileged tf03mqtt:latest 7 | 8 | pibin: 9 | env GOOS=linux GOARCH=arm GOARM=7 go build 10 | 11 | clean: 12 | docker stop tf03mqtt_agent 13 | docker rm tf03mqtt_agent 14 | 15 | .PHONY: docker clean run 16 | -------------------------------------------------------------------------------- /apache-license-2.0.md: -------------------------------------------------------------------------------- 1 | ``` 2 | ------------------------------------------------------------------------- 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | 8 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 9 | 10 | 1. Definitions. 11 | 12 | "License" shall mean the terms and conditions for use, reproduction, 13 | and distribution as defined by Sections 1 through 9 of this document. 14 | 15 | "Licensor" shall mean the copyright owner or entity authorized by 16 | the copyright owner that is granting the License. 17 | 18 | "Legal Entity" shall mean the union of the acting entity and all 19 | other entities that control, are controlled by, or are under common 20 | control with that entity. For the purposes of this definition, 21 | "control" means (i) the power, direct or indirect, to cause the 22 | direction or management of such entity, whether by contract or 23 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 24 | outstanding shares, or (iii) beneficial ownership of such entity. 25 | 26 | "You" (or "Your") shall mean an individual or Legal Entity 27 | exercising permissions granted by this License. 28 | 29 | "Source" form shall mean the preferred form for making modifications, 30 | including but not limited to software source code, documentation 31 | source, and configuration files. 32 | 33 | "Object" form shall mean any form resulting from mechanical 34 | transformation or translation of a Source form, including but 35 | not limited to compiled object code, generated documentation, 36 | and conversions to other media types. 37 | 38 | "Work" shall mean the work of authorship, whether in Source or 39 | Object form, made available under the License, as indicated by a 40 | copyright notice that is included in or attached to the work 41 | (an example is provided in the Appendix below). 42 | 43 | "Derivative Works" shall mean any work, whether in Source or Object 44 | form, that is based on (or derived from) the Work and for which the 45 | editorial revisions, annotations, elaborations, or other modifications 46 | represent, as a whole, an original work of authorship. For the purposes 47 | of this License, Derivative Works shall not include works that remain 48 | separable from, or merely link (or bind by name) to the interfaces of, 49 | the Work and Derivative Works thereof. 50 | 51 | "Contribution" shall mean any work of authorship, including 52 | the original version of the Work and any modifications or additions 53 | to that Work or Derivative Works thereof, that is intentionally 54 | submitted to Licensor for inclusion in the Work by the copyright owner 55 | or by an individual or Legal Entity authorized to submit on behalf of 56 | the copyright owner. For the purposes of this definition, "submitted" 57 | means any form of electronic, verbal, or written communication sent 58 | to the Licensor or its representatives, including but not limited to 59 | communication on electronic mailing lists, source code control systems, 60 | and issue tracking systems that are managed by, or on behalf of, the 61 | Licensor for the purpose of discussing and improving the Work, but 62 | excluding communication that is conspicuously marked or otherwise 63 | designated in writing by the copyright owner as "Not a Contribution." 64 | 65 | "Contributor" shall mean Licensor and any individual or Legal Entity 66 | on behalf of whom a Contribution has been received by Licensor and 67 | subsequently incorporated within the Work. 68 | 69 | 2. Grant of Copyright License. Subject to the terms and conditions of 70 | this License, each Contributor hereby grants to You a perpetual, 71 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 72 | copyright license to reproduce, prepare Derivative Works of, 73 | publicly display, publicly perform, sublicense, and distribute the 74 | Work and such Derivative Works in Source or Object form. 75 | 76 | 3. Grant of Patent License. Subject to the terms and conditions of 77 | this License, each Contributor hereby grants to You a perpetual, 78 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 79 | (except as stated in this section) patent license to make, have made, 80 | use, offer to sell, sell, import, and otherwise transfer the Work, 81 | where such license applies only to those patent claims licensable 82 | by such Contributor that are necessarily infringed by their 83 | Contribution(s) alone or by combination of their Contribution(s) 84 | with the Work to which such Contribution(s) was submitted. If You 85 | institute patent litigation against any entity (including a 86 | cross-claim or counterclaim in a lawsuit) alleging that the Work 87 | or a Contribution incorporated within the Work constitutes direct 88 | or contributory patent infringement, then any patent licenses 89 | granted to You under this License for that Work shall terminate 90 | as of the date such litigation is filed. 91 | 92 | 4. Redistribution. You may reproduce and distribute copies of the 93 | Work or Derivative Works thereof in any medium, with or without 94 | modifications, and in Source or Object form, provided that You 95 | meet the following conditions: 96 | 97 | (a) You must give any other recipients of the Work or 98 | Derivative Works a copy of this License; and 99 | 100 | (b) You must cause any modified files to carry prominent notices 101 | stating that You changed the files; and 102 | 103 | (c) You must retain, in the Source form of any Derivative Works 104 | that You distribute, all copyright, patent, trademark, and 105 | attribution notices from the Source form of the Work, 106 | excluding those notices that do not pertain to any part of 107 | the Derivative Works; and 108 | 109 | (d) If the Work includes a "NOTICE" text file as part of its 110 | distribution, then any Derivative Works that You distribute must 111 | include a readable copy of the attribution notices contained 112 | within such NOTICE file, excluding those notices that do not 113 | pertain to any part of the Derivative Works, in at least one 114 | of the following places: within a NOTICE text file distributed 115 | as part of the Derivative Works; within the Source form or 116 | documentation, if provided along with the Derivative Works; or, 117 | within a display generated by the Derivative Works, if and 118 | wherever such third-party notices normally appear. The contents 119 | of the NOTICE file are for informational purposes only and 120 | do not modify the License. You may add Your own attribution 121 | notices within Derivative Works that You distribute, alongside 122 | or as an addendum to the NOTICE text from the Work, provided 123 | that such additional attribution notices cannot be construed 124 | as modifying the License. 125 | 126 | You may add Your own copyright statement to Your modifications and 127 | may provide additional or different license terms and conditions 128 | for use, reproduction, or distribution of Your modifications, or 129 | for any such Derivative Works as a whole, provided Your use, 130 | reproduction, and distribution of the Work otherwise complies with 131 | the conditions stated in this License. 132 | 133 | 5. Submission of Contributions. Unless You explicitly state otherwise, 134 | any Contribution intentionally submitted for inclusion in the Work 135 | by You to the Licensor shall be under the terms and conditions of 136 | this License, without any additional terms or conditions. 137 | Notwithstanding the above, nothing herein shall supersede or modify 138 | the terms of any separate license agreement you may have executed 139 | with Licensor regarding such Contributions. 140 | 141 | 6. Trademarks. This License does not grant permission to use the trade 142 | names, trademarks, service marks, or product names of the Licensor, 143 | except as required for reasonable and customary use in describing the 144 | origin of the Work and reproducing the content of the NOTICE file. 145 | 146 | 7. Disclaimer of Warranty. Unless required by applicable law or 147 | agreed to in writing, Licensor provides the Work (and each 148 | Contributor provides its Contributions) on an "AS IS" BASIS, 149 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 150 | implied, including, without limitation, any warranties or conditions 151 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 152 | PARTICULAR PURPOSE. You are solely responsible for determining the 153 | appropriateness of using or redistributing the Work and assume any 154 | risks associated with Your exercise of permissions under this License. 155 | 156 | 8. Limitation of Liability. In no event and under no legal theory, 157 | whether in tort (including negligence), contract, or otherwise, 158 | unless required by applicable law (such as deliberate and grossly 159 | negligent acts) or agreed to in writing, shall any Contributor be 160 | liable to You for damages, including any direct, indirect, special, 161 | incidental, or consequential damages of any character arising as a 162 | result of this License or out of the use or inability to use the 163 | Work (including but not limited to damages for loss of goodwill, 164 | work stoppage, computer failure or malfunction, or any and all 165 | other commercial damages or losses), even if such Contributor 166 | has been advised of the possibility of such damages. 167 | 168 | 9. Accepting Warranty or Additional Liability. While redistributing 169 | the Work or Derivative Works thereof, You may choose to offer, 170 | and charge a fee for, acceptance of support, warranty, indemnity, 171 | or other liability obligations and/or rights consistent with this 172 | License. However, in accepting such obligations, You may act only 173 | on Your own behalf and on Your sole responsibility, not on behalf 174 | of any other Contributor, and only if You agree to indemnify, 175 | defend, and hold each Contributor harmless for any liability 176 | incurred by, or claims asserted against, such Contributor by reason 177 | of your accepting any such warranty or additional liability. 178 | 179 | END OF TERMS AND CONDITIONS 180 | ``` -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "gopkg.in/yaml.v2" 5 | "io/ioutil" 6 | ) 7 | 8 | type sensor struct { 9 | Name string `yaml:"name"` 10 | Unit string `yaml:"unit"` 11 | Icon string `yaml:"icon"` 12 | StateTopic string `yaml:"state_topic"` 13 | ValueTemplate string `yaml:"value_template"` 14 | } 15 | 16 | type config struct { 17 | Name string `yaml:"name"` 18 | Manufacturer string `yaml:"manufacturer"` 19 | Model string `yaml:"model"` 20 | MqttServer string `yaml:"mqtt_server"` 21 | SerialPort string `yaml:"serial_port"` 22 | Topic string `yaml:"topic"` 23 | HaRegister bool `yaml:"ha_register"` 24 | Sensors []sensor `yaml:"sensors"` 25 | } 26 | 27 | func parseYaml(path string, c interface{}) error { 28 | cfg, err := ioutil.ReadFile(path) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | err = yaml.Unmarshal(cfg, c) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | name: tf03k 2 | manufacturer: Baiway 3 | model: tf03k coulomb meter 4 | mqtt_server: tcp://192.168.5.180:1883 5 | ha_register: true 6 | serial_port: /dev/ttyUSB0 7 | topic: homeassistant/sensor/tf03k/state 8 | 9 | sensors: 10 | - name: Battery_soc 11 | unit: "%" 12 | icon: mdi:battery 13 | state_topic: homeassistant/sensor/tf03k/state 14 | value_template: "{{ value_json.soc }}" 15 | - name: Battery_voltage 16 | unit: "V" 17 | icon: mdi:power-plug 18 | state_topic: homeassistant/sensor/tf03k/state 19 | value_template: "{{ (value_json.decivolt / 100.0) | round(1) }}" 20 | - name: Battery_capacity 21 | unit: "Ah" 22 | icon: mdi:current-dc 23 | state_topic: homeassistant/sensor/tf03k/state 24 | value_template: "{{ (value_json.capamah / 1000.0) | round(1) }}" 25 | - name: Battery_current 26 | unit: "A" 27 | icon: mdi:current-dc 28 | state_topic: homeassistant/sensor/tf03k/state 29 | value_template: "{{ (value_json.currentma / 1000.0) | round(1) }}" 30 | - name: Battery_power 31 | unit: "W" 32 | icon: mdi:current-dc 33 | state_topic: homeassistant/sensor/tf03k/state 34 | value_template: "{{ (value_json.currentma * value_json.decivolt / 100000.0) | round(0) }}" 35 | - name: Battery_sec 36 | unit: "S" 37 | icon: mdi:timer 38 | state_topic: homeassistant/sensor/tf03k/state 39 | value_template: "{{ value_json.remainsec }}" 40 | -------------------------------------------------------------------------------- /coulombmeter.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | MQTT "github.com/eclipse/paho.mqtt.golang" 5 | log "github.com/sirupsen/logrus" 6 | "github.com/tarm/serial" 7 | "io" 8 | "os" 9 | "time" 10 | ) 11 | 12 | // parse state 13 | const ( 14 | // wait start of frame 15 | sof = iota 16 | // wait soc 17 | soc 18 | // wait voltage 19 | voltage 20 | // wait capacity 21 | capacity 22 | // wait current 23 | current 24 | // wait time remaining 25 | remaining 26 | // wait crc 27 | crc 28 | ) 29 | 30 | // tf03 frame content 31 | type frame struct { 32 | Soc uint8 `json:"soc"` 33 | DeciVolt uint16 `json:"decivolt"` 34 | CapaMAh uint32 `json:"capamah"` 35 | CurrentMA int32 `json:"currentma"` 36 | RemainSec uint32 `json:"remainsec"` 37 | Sum uint8 `json:"-"` 38 | } 39 | 40 | type Coulombmeter struct { 41 | dev *serial.Port 42 | port string 43 | } 44 | 45 | // init serial port 46 | func (cm *Coulombmeter) init() error { 47 | var err error 48 | 49 | c := &serial.Config{Name: cm.port, Baud: 9600, ReadTimeout: 2 * time.Second} 50 | cm.dev, err = serial.OpenPort(c) 51 | if err != nil { 52 | return err 53 | } 54 | // flush device buffers 55 | rs := make([]byte, 300) 56 | _, _ = cm.dev.Read(rs) 57 | return nil 58 | } 59 | 60 | // frame parser goroutine 61 | func (cm *Coulombmeter) parseFrame(ch chan uint8, ct MQTT.Client) { 62 | var f frame 63 | state := sof 64 | var shift = 0 65 | 66 | for c := range ch { 67 | switch state { 68 | case sof: 69 | if c == 0xA5 { 70 | state = soc 71 | f.Sum = 0xA5 72 | } 73 | // ignore byte 74 | continue 75 | case soc: 76 | f.Soc = c 77 | f.Sum += c 78 | state = voltage 79 | shift = 8 80 | continue 81 | case voltage: 82 | f.DeciVolt = f.DeciVolt | (uint16(c) << shift) 83 | f.Sum += c 84 | shift -= 8 85 | if shift < 0 { 86 | state = capacity 87 | shift = 24 88 | } 89 | continue 90 | case capacity: 91 | f.CapaMAh = f.CapaMAh | (uint32(c) << shift) 92 | f.Sum += c 93 | shift -= 8 94 | if shift < 0 { 95 | state = current 96 | shift = 24 97 | } 98 | continue 99 | case current: 100 | f.CurrentMA = f.CurrentMA | (int32(c) << shift) 101 | f.Sum += c 102 | shift -= 8 103 | if shift < 0 { 104 | state = remaining 105 | shift = 16 106 | } 107 | continue 108 | case remaining: 109 | f.RemainSec = f.RemainSec | (uint32(c) << shift) 110 | f.Sum += c 111 | shift -= 8 112 | if shift < 0 { 113 | state = crc 114 | } 115 | continue 116 | case crc: 117 | if f.Sum == c { 118 | err := publishAsJson(cfg.Topic, f, ct) 119 | if err != nil { 120 | log.Errorln(err) 121 | os.Exit(-3) 122 | } 123 | } 124 | state = sof 125 | shift = 0 126 | f.reset() 127 | continue 128 | } 129 | } 130 | } 131 | 132 | // reset frame 133 | func (f *frame) reset() { 134 | f.RemainSec = 0 135 | f.CurrentMA = 0 136 | f.CapaMAh = 0 137 | f.DeciVolt = 0 138 | f.Soc = 0 139 | f.Sum = 0 140 | } 141 | 142 | // serial port infinite reader 143 | func (cm *Coulombmeter) poll(ch chan uint8) error { 144 | rs := make([]byte, 64) 145 | defer close(ch) 146 | for true { 147 | n, err := cm.dev.Read(rs) 148 | if err == io.EOF || n == 0 { 149 | log.Warnln("timeout") 150 | continue 151 | } 152 | if err != nil { 153 | log.Errorln(err) 154 | return err 155 | } 156 | for i := 0; i < n; i++ { 157 | ch <- rs[i] 158 | } 159 | } 160 | return nil 161 | } 162 | 163 | -------------------------------------------------------------------------------- /docs/TF03K communication specification.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edillmann/tf03mqtt/487799766d4f7fc128c3d9ca8db7eb45ffa289b6/docs/TF03K communication specification.pdf -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/edillmann/tf03mqtt 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/eclipse/paho.mqtt.golang v1.3.0 7 | github.com/sirupsen/logrus v1.7.0 8 | github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 9 | gopkg.in/yaml.v2 v2.4.0 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/eclipse/paho.mqtt.golang v1.2.0 h1:1F8mhG9+aO5/xpdtFkW4SxOJB67ukuDC3t2y2qayIX0= 4 | github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts= 5 | github.com/eclipse/paho.mqtt.golang v1.3.0 h1:MU79lqr3FKNKbSrGN7d7bNYqh8MwWW7Zcx0iG+VIw9I= 6 | github.com/eclipse/paho.mqtt.golang v1.3.0/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc= 7 | github.com/edillmann/hid v1.0.1 h1:BHazHl7kHROV2WPaRMEWM9j5l/horrbFqeFdKBD/QrM= 8 | github.com/edillmann/hid v1.0.1/go.mod h1:g9M5JAGsMd4MW3QFysgv5sS2/jrz6qA4C54AGH+sV+w= 9 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 10 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= 14 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 15 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 16 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 17 | github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU= 18 | github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= 19 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 20 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 21 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 22 | golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U= 23 | golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 24 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U= 25 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 26 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 27 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 28 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 29 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= 30 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 31 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= 32 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 33 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 34 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 35 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 36 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 37 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 38 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 39 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 40 | -------------------------------------------------------------------------------- /images/ha_integration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edillmann/tf03mqtt/487799766d4f7fc128c3d9ca8db7eb45ffa289b6/images/ha_integration.png -------------------------------------------------------------------------------- /images/tf03k.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edillmann/tf03mqtt/487799766d4f7fc128c3d9ca8db7eb45ffa289b6/images/tf03k.png -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | log "github.com/sirupsen/logrus" 6 | "time" 7 | ) 8 | 9 | type formatter struct { 10 | } 11 | 12 | func (f formatter) Format(e *log.Entry) ([]byte, error) { 13 | return []byte(fmt.Sprintln(e.Time.Format(time.StampMilli), " ", e.Message)), nil 14 | } 15 | 16 | func setLogLevel(level string) { 17 | lvl, err := log.ParseLevel(level) 18 | if err != nil { 19 | log.Warnln(err, "Fallback to info level") 20 | return 21 | } 22 | log.SetLevel(lvl) 23 | } 24 | 25 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | MQTT "github.com/eclipse/paho.mqtt.golang" 6 | log "github.com/sirupsen/logrus" 7 | "os" 8 | ) 9 | 10 | var cfg config 11 | 12 | //define a function for the default message handler 13 | var f MQTT.MessageHandler = func(client MQTT.Client, msg MQTT.Message) { 14 | log.Warn("TOPIC: %s\n", msg.Topic()) 15 | log.Warn("MSG: %s\n", msg.Payload()) 16 | } 17 | 18 | func main() { 19 | 20 | var loglevel = flag.String("loglevel", "info", "log level [trace,debug,info,warn,error,fatal,panic]") 21 | var configFile = flag.String("config", "config.yaml", "config file") 22 | flag.Parse() 23 | 24 | // configure log formatter 25 | setLogLevel(*loglevel) 26 | log.SetFormatter(&formatter{}) 27 | 28 | err := parseYaml(*configFile, &cfg) 29 | 30 | if err != nil { 31 | log.Error(err.Error()) 32 | os.Exit(-1) 33 | } 34 | 35 | opts := MQTT.NewClientOptions().AddBroker(cfg.MqttServer) 36 | opts.SetClientID(cfg.Name) 37 | opts.SetDefaultPublishHandler(f) 38 | 39 | //create and start a client using the above ClientOptions 40 | c := MQTT.NewClient(opts) 41 | if token := c.Connect(); token.Wait() && token.Error() != nil { 42 | panic(token.Error()) 43 | } 44 | 45 | // allocate coulometer 46 | cm := Coulombmeter{ 47 | port: cfg.SerialPort, 48 | } 49 | // register sensors to Home Assistant 50 | if cfg.HaRegister { 51 | cm.register(c) 52 | } 53 | 54 | // init serial port 55 | err = cm.init() 56 | if err != nil { 57 | log.Errorln(err) 58 | os.Exit(-1) 59 | } 60 | 61 | // allocate channel to store frame 62 | ch := make(chan uint8, 64) 63 | // start frame parser goroutine 64 | go cm.parseFrame(ch, c) 65 | // infinite loop reading on serial port 66 | err = cm.poll(ch) 67 | if err != nil { 68 | log.Errorln(err) 69 | os.Exit(-2) 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /mqtt.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | MQTT "github.com/eclipse/paho.mqtt.golang" 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | func publishAsJson(topic string, s interface{}, c MQTT.Client) error { 10 | buf, err := json.Marshal(s) 11 | json := string(buf) 12 | log.Trace(json) 13 | c.Publish(topic, 0, false, json) 14 | return err 15 | } 16 | -------------------------------------------------------------------------------- /mqtt_register.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | MQTT "github.com/eclipse/paho.mqtt.golang" 5 | "strings" 6 | ) 7 | 8 | type sensorReg struct { 9 | Name string `json:"name"` 10 | UnitOfMeasurement string `json:"unit_of_measurement,omit_empty"` 11 | Icon string `json:"icon,omit_empty"` 12 | StateTopic string `json:"state_topic,omit_empty"` 13 | ValueTemplate string `json:"value_template,omit_empty"` 14 | ExpireAfter uint32 `json:"expire_after"` 15 | UniqueId string `json:"unique_id"` 16 | Platform string `json:"platform"` 17 | Device struct { 18 | Name string `json:"name"` 19 | Manufacturer string `json:"manufacturer"` 20 | Model string `json:"model"` 21 | Identifiers []string `json:"identifiers"` 22 | } `json:"device"` 23 | } 24 | 25 | func (cm *Coulombmeter) register(c MQTT.Client) { 26 | for _, s := range cfg.Sensors { 27 | var r sensorReg 28 | r.Name = s.Name 29 | r.UnitOfMeasurement = s.Unit 30 | r.Icon = s.Icon 31 | r.StateTopic = s.StateTopic 32 | r.ValueTemplate = s.ValueTemplate 33 | r.ExpireAfter = uint32(60) 34 | r.UniqueId = strings.ToLower(cfg.Name + "_" + s.Name) 35 | r.Platform = "mqtt" 36 | r.Device.Name = cfg.Name 37 | r.Device.Manufacturer = cfg.Manufacturer 38 | r.Device.Model = cfg.Model 39 | r.Device.Identifiers = append(r.Device.Identifiers, strings.ToLower(cfg.Name)) 40 | 41 | _ = publishAsJson(strings.ToLower("homeassistant/sensor/"+r.Name+"/config"), r, c) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # TF03K serial to MQTT 2 | 3 | This projet aim's to export TF03K coulomb meter information 4 | to MQTT and home assistant 5 | 6 | ![](images/tf03k.png) 7 | 8 | ## Getting Started 9 | 10 | ### Have a tk03k coulomb meter 11 | 12 | You can find one on aliexpress : https://fr.aliexpress.com/item/32883704077.html 13 | 14 | Have a usb to serial ttl : https://fr.aliexpress.com/item/32964764260.html 15 | 16 | # Wiring 17 | 18 | See ![TF03K communication specifications.pdf](docs/TF03K%20communication%20specification.pdf) 19 | 20 | ### Prerequisites 21 | 22 | This software was written with docker in mind, the software itself 23 | is written in go and should run on any platform where go is running 24 | 25 | You can find instruction to install and configure docker here: 26 | ``` 27 | https://docs.docker.com/engine/install/debian 28 | ``` 29 | 30 | ### Configuration 31 | 32 | To configure you docker image you have to setup the following entries 33 | in the supplied config.yaml 34 | 35 | ``` 36 | name: tf03k 37 | manufacturer: Baiway 38 | model: tf03k 39 | mqtt_server: tcp://192.168.5.180:1883 40 | serial_port: /dev/ttyUSB0 41 | ``` 42 | 43 | ### Installing 44 | 45 | ``` 46 | make clean docker run 47 | ``` 48 | 49 | ### HA Intégration 50 | 51 | ![](images/ha_integration.png) 52 | 53 | ## Tested platforms 54 | 55 | * docker 19.03.8 56 | * Raspberry PI 2+ 57 | * Raspberry PI 4 58 | * Rockpi 4 59 | * Synology X86 based 60 | 61 | ## MQTT message 62 | 63 | This is an example of the json message sent to MQTT: 64 | ``` 65 | {"soc":10,"decivolt":5101,"capamah":20118,"currentma":-1397,"remainsec":52030} 66 | ``` 67 | 68 | ## Debugging 69 | 70 | ... to be written 71 | 72 | ## TODO 73 | 74 | * handle MQTT user / password 75 | * enhance documentation 76 | * add lovelace card template 77 | 78 | ## Authors 79 | 80 | * **Eric Dillmann** - *Initial work* - [edillmann](https://github.com/edillmann) 81 | 82 | ## License 83 | 84 | This project is licensed under the Apache 2 License - see the [apache-license-2.0.md](apache-license-2.0.md) file for details 85 | 86 | ## Acknowledgments 87 | 88 | 89 | --------------------------------------------------------------------------------