iobroker.leapmotor 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Henrik Schönhofen (backfisch88)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ ![Logo](admin/leapmotor.png)
2
+
3
+ # ioBroker.leapmotor
4
+
5
+ [![Version](https://img.shields.io/badge/version-0.2.0-blue.svg)](https://github.com/backfisch88/ioBroker.leapmotor)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ Unofficial Leapmotor electric vehicle integration for ioBroker. Tested on T03.
9
+
10
+ ## ⚠️ Important: Use a Second Account
11
+
12
+ **Do not use your main Leapmotor account!**
13
+
14
+ The adapter maintains a permanent session with the Leapmotor cloud. If the same account is used simultaneously in the Leapmotor app, both sessions will conflict and log each other out.
15
+
16
+ **Recommended setup:**
17
+
18
+ 1. Create a second Leapmotor account (e.g. with a second email address)
19
+ 1. In the Leapmotor app, navigate to:
20
+ **Personal Center → My Vehicle → [Vehicle Name] → Shared Members → Add Shared Member**
21
+ 1. Enter the second account’s email and grant all rights
22
+ 1. Use the second account credentials in the adapter configuration
23
+
24
+ This way your main account stays logged in to the app at all times.
25
+
26
+ -----
27
+
28
+ ## Features
29
+
30
+ - Vehicle status polling every 1–60 minutes (configurable)
31
+ - Battery SOC, range, temperature, tire pressure, GPS, doors, windows
32
+ - Remote control: climate (heat/cool/vent), lock/unlock, windows, trunk, find
33
+ - Consumption statistics with weekly history
34
+ - Dynamic vehicle dashboard (composite HTML widget for VIS)
35
+ - Automatic token refresh
36
+ - Picture cache (downloaded once, stored locally)
37
+
38
+ ## Tested Vehicles
39
+
40
+ - Leapmotor T03 ✅
41
+
42
+ ## Installation
43
+
44
+ Install via ioBroker Admin UI or use:
45
+
46
+ ```bash
47
+ iobroker url https://github.com/backfisch88/ioBroker.leapmotor
48
+ ```
49
+
50
+ ## Configuration
51
+
52
+ |Setting |Description |
53
+ |----------------|--------------------------------------------------------------------|
54
+ |Email |Leapmotor account email (recommend using a dedicated second account)|
55
+ |Password |Leapmotor account password |
56
+ |Vehicle PIN |4-digit vehicle PIN – required for all remote commands |
57
+ |Polling interval|Status update interval in minutes (default: 5) |
58
+
59
+ ## Datapoints
60
+
61
+ ```
62
+ leapmotor.0.<VIN>.status.* → Vehicle status (read-only)
63
+ leapmotor.0.<VIN>.consumption.* → Consumption & statistics (read-only)
64
+ leapmotor.0.<VIN>.pictures.* → Vehicle images (read-only)
65
+ leapmotor.0.<VIN>.pictures.composite_html → Full dashboard HTML widget
66
+ leapmotor.0.<VIN>.cmd.* → Commands (writable)
67
+ ```
68
+
69
+ ### VIS Dashboard Widget
70
+
71
+ Add a **basic - string (unescaped)** widget in VIS and set the Object ID to:
72
+
73
+ ```
74
+ leapmotor.0.<VIN>.pictures.composite_html
75
+ ```
76
+
77
+ ### Available Commands
78
+
79
+ |Command |Description |PIN required|
80
+ |-----------------------|-------------------------------|:----------:|
81
+ |cmd.ac_heiz |Start heating |✅ |
82
+ |cmd.ac_kuehl |Start cooling |✅ |
83
+ |cmd.ac_luft |Start ventilation |✅ |
84
+ |cmd.ac_off |Stop climate |✅ |
85
+ |cmd.ac_temp |Target temperature (16–30°C) |– |
86
+ |cmd.defrost |Windshield defrost |✅ |
87
+ |cmd.lock |Lock vehicle |✅ |
88
+ |cmd.unlock |Unlock vehicle |✅ |
89
+ |cmd.trunk_open |Open trunk |✅ |
90
+ |cmd.trunk_close |Close trunk |✅ |
91
+ |cmd.windows_open |Open windows |– |
92
+ |cmd.windows_close |Close windows |– |
93
+ |cmd.find |Find vehicle (horn/lights) |– |
94
+ |cmd.battery_preheat |Battery preheat on |✅ |
95
+ |cmd.battery_preheat_off|Battery preheat off |✅ |
96
+ |cmd.refresh |Trigger immediate status update|– |
97
+
98
+ ## Changelog
99
+
100
+ ### 0.2.0 (2026-06-10)
101
+
102
+ - Full release: status, consumption, pictures, composite HTML dashboard
103
+ - Automatic token refresh
104
+ - Picture cache
105
+ - All remote commands require PIN verification
106
+
107
+ ### 0.1.0 (2026-06-09)
108
+
109
+ - Initial release
110
+
111
+ ## Legal Notice
112
+
113
+ This adapter is **not affiliated with, endorsed by, or officially connected to Leapmotor** or any of its subsidiaries or affiliates.
114
+
115
+ The adapter communicates with Leapmotor’s cloud API using credentials provided by the user. All trademarks, service marks, product names, and company names or logos mentioned are the property of their respective owners.
116
+
117
+ Use at your own risk. The authors accept no liability for any damage, data loss, or other issues caused by the use of this software.
118
+
119
+ ## License
120
+
121
+ MIT License
122
+
123
+ Copyright (c) 2026 Henrik Schönhofen (backfisch88)
@@ -0,0 +1,46 @@
1
+ {
2
+ "type": "panel",
3
+ "items": {
4
+ "email": {
5
+ "type": "text",
6
+ "label": "Leapmotor E-Mail",
7
+ "help": "Use a dedicated second account (see README)",
8
+ "newLine": true,
9
+ "xs": 12, "sm": 12, "md": 12, "lg": 12, "xl": 12
10
+ },
11
+ "password": {
12
+ "type": "password",
13
+ "label": "Password",
14
+ "newLine": false,
15
+ "xs": 12, "sm": 12, "md": 12, "lg": 12, "xl": 12
16
+ },
17
+ "operationPassword": {
18
+ "type": "password",
19
+ "label": "Vehicle PIN",
20
+ "help": "4-digit PIN required for all remote commands",
21
+ "newLine": true,
22
+ "xs": 12, "sm": 12, "md": 12, "lg": 12, "xl": 12
23
+ },
24
+ "pollingInterval": {
25
+ "type": "number",
26
+ "label": "Polling interval (minutes)",
27
+ "help": "How often to fetch vehicle status (1-60)",
28
+ "min": 1,
29
+ "max": 60,
30
+ "newLine": true,
31
+ "xs": 12, "sm": 6, "md": 6, "lg": 6, "xl": 6
32
+ },
33
+ "language": {
34
+ "type": "select",
35
+ "label": "Language",
36
+ "options": [
37
+ {"label": "English", "value": "en-GB"},
38
+ {"label": "Deutsch", "value": "de"},
39
+ {"label": "Français", "value": "fr"},
40
+ {"label": "Italiano", "value": "it"}
41
+ ],
42
+ "newLine": false,
43
+ "xs": 12, "sm": 6, "md": 6, "lg": 6, "xl": 6
44
+ }
45
+ }
46
+ }
Binary file
@@ -0,0 +1,9 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
2
+ <rect width="100" height="100" rx="15" fill="#1a73e8"/>
3
+ <text x="50" y="55" font-family="Arial" font-size="14" font-weight="bold"
4
+ text-anchor="middle" fill="white">LEAP</text>
5
+ <text x="50" y="72" font-family="Arial" font-size="10"
6
+ text-anchor="middle" fill="#aaddff">MOTOR</text>
7
+ <circle cx="50" cy="28" r="12" fill="none" stroke="white" stroke-width="2"/>
8
+ <path d="M45 28 L50 22 L55 28" stroke="white" stroke-width="2" fill="none"/>
9
+ </svg>
@@ -0,0 +1,161 @@
1
+ 'use strict';
2
+ Object.defineProperty(exports,"__esModule",{value:true});
3
+ const crypto=require('crypto');
4
+ const https=require('https');
5
+ const forge=require('node-forge');
6
+ const axios=require('axios');
7
+ const {deriveAccountP12Password,deriveSignKey,deriveSessionDeviceId,encryptOperatePassword,buildLoginHeaders,buildSignedHeaders,buildRemoteCtlWriteHeaders,buildRemoteCtlWriteHeadersWithoutPin,buildRemoteCtlResultHeaders,buildOperpwdVerifyHeaders}=require('./leapmotor-crypto');
8
+ const KNOWN_PASSWORDS=['','leapmotor','leap123'];
9
+ class LeapmotorClient{
10
+ constructor(config){
11
+ this.userId=null;this.token=null;this.signIkm=null;this.signSalt=null;this.signInfo=null;
12
+ this.accountCertPem=null;this.accountKeyPem=null;
13
+ this.config={baseUrl:config.baseUrl||'https://appgateway.leapmotor-international.de',timeout:config.timeout||30000,language:config.language||'en-GB',operationPassword:config.operationPassword||'',...config};
14
+ this.deviceId=crypto.randomUUID().replace(/-/g,'');
15
+ this.appCertPem=config.appCertPem;
16
+ this.appKeyPem=config.appKeyPem;
17
+ this.http=this._createHttpClient(config.appCertPem,config.appKeyPem);
18
+ }
19
+ _createHttpClient(certPem,keyPem){const agent=new https.Agent({cert:certPem,key:keyPem,rejectUnauthorized:false});return axios.create({baseURL:this.config.baseUrl,timeout:this.config.timeout,httpsAgent:agent,headers:{'Content-Type':'application/x-www-form-urlencoded'}})}
20
+ get _signKey(){if(!this.signIkm)throw new Error('Not authenticated');return Buffer.from(deriveSignKey(this.signIkm,this.signSalt,this.signInfo))}
21
+ _authHeaders(){if(!this.userId||!this.token)throw new Error('Not authenticated');return{userId:this.userId,token:this.token}}
22
+ _h(h){return{nonce:h.nonce,deviceId:h.deviceId,timestamp:h.timestamp,sign:h.sign,acceptLanguage:h.acceptLanguage,channel:'1',source:'leapmotor',deviceType:'1',version:'1.12.3'}}
23
+ _parseResponse(data,label){if(data.code!==0){const msg=String(data.message||JSON.stringify(data));throw new Error(`${label} failed: ${msg}`)}return data}
24
+ _loadAccountCert(ld){
25
+ const p12Der=Buffer.from(String(ld.base64Cert||''),'base64');
26
+ const p12Asn1=forge.asn1.fromDer(forge.util.createBuffer(p12Der));
27
+ const pwds=[];
28
+ try{pwds.push(deriveAccountP12Password(String(ld.id),String(ld.uid)))}catch{}
29
+ pwds.push(...KNOWN_PASSWORDS);
30
+ for(const pwd of pwds){try{const p12=forge.pkcs12.pkcs12FromAsn1(p12Asn1,pwd);const cb=p12.getBags({bagType:forge.pki.oids.certBag});const kb=p12.getBags({bagType:forge.pki.oids.pkcs8ShroudedKeyBag});const cert=cb[forge.pki.oids.certBag]?.[0]?.cert;const key=kb[forge.pki.oids.pkcs8ShroudedKeyBag]?.[0]?.key;if(cert&&key){this.accountCertPem=forge.pki.certificateToPem(cert);this.accountKeyPem=forge.pki.privateKeyToPem(key);return}}catch{}}
31
+ throw new Error('Could not open account certificate');
32
+ }
33
+ async login(){
34
+ const h=this._h(buildLoginHeaders({deviceId:this.deviceId,username:this.config.username,password:this.config.password,language:this.config.language}));
35
+ const body=new URLSearchParams({isRecoverAcct:'0',password:this.config.password,policyId:'20260204',loginMethod:'1',email:this.config.username}).toString();
36
+ const loginHttp=this.appHttp||this.http;
37
+ const resp=await loginHttp.post('/carownerservice/oversea/acct/v1/login',body,{headers:h});
38
+ const data=this._parseResponse(resp.data,'login');const ld=data.data||{};
39
+ this.userId=String(ld.id);this.token=String(ld.token);
40
+ this.deviceId=deriveSessionDeviceId(this.token,this.deviceId);
41
+ this.signIkm=String(ld.signIkm);this.signSalt=String(ld.signSalt);this.signInfo=String(ld.signInfo);
42
+ this._loadAccountCert(ld);
43
+ this.http=this._createHttpClient(this.accountCertPem,this.accountKeyPem);
44
+ this.appHttp=this._createHttpClient(this.appCertPem,this.appKeyPem);
45
+ }
46
+ async getVehicleList(){
47
+ const h={...this._h(buildSignedHeaders({signKey:this._signKey,deviceId:this.deviceId,language:this.config.language})),...this._authHeaders()};
48
+ const resp=await this.http.post('/carownerservice/oversea/vehicle/v1/list','',{headers:h});
49
+ const data=this._parseResponse(resp.data,'vehicle list');
50
+ const d=data.data||{};
51
+ // API gibt bindcars (eigene) oder sharedcars (geteilte) zurück
52
+ const list=Array.isArray(d)?d:[...(d.bindcars||[]),...(d.sharedcars||[])];
53
+ return list.filter(i=>i?.vin).map(i=>({
54
+ vin:i.vin,
55
+ carType:i.carType||i.carAlias||'T03',
56
+ name:i.vinNickname||i.name||i.vin,
57
+ raw:i
58
+ }));
59
+ }
60
+ async getVehicleStatus(vehicle){
61
+ const ct=vehicle.carType.trim().toLowerCase();
62
+ const h={...this._h(buildSignedHeaders({signKey:this._signKey,deviceId:this.deviceId,vin:vehicle.vin,language:this.config.language})),...this._authHeaders()};
63
+ const resp=await this.http.post(`/carownerservice/oversea/vehicle/v1/status/get/${ct}`,`vin=${encodeURIComponent(vehicle.vin)}`,{headers:h});
64
+ return this._parseResponse(resp.data,'vehicle status').data||{};
65
+ }
66
+ async sendCommandWithoutPin(vehicle,cmdId,cmdContent){
67
+ const h={...this._h(buildRemoteCtlWriteHeadersWithoutPin({signKey:this._signKey,deviceId:this.deviceId,vin:vehicle.vin,cmdContent,cmdId,language:this.config.language})),...this._authHeaders()};
68
+ const body=`cmdContent=${encodeURIComponent(cmdContent)}&vin=${encodeURIComponent(vehicle.vin)}&cmdId=${encodeURIComponent(cmdId)}`;
69
+ const resp=await this.http.post('/carownerservice/oversea/vehicle/v1/app/remote/ctl',body,{headers:h});
70
+ const result=this._parseResponse(resp.data,`remote ${cmdId}`);await this._pollResult(result);return result;
71
+ }
72
+ async sendCommandWithPin(vehicle,cmdId,cmdContent){
73
+ if(!this.config.operationPassword)throw new Error('operationPassword required');
74
+ const ep=encryptOperatePassword(this.config.operationPassword,this.token);
75
+ const vh={...this._h(buildOperpwdVerifyHeaders({signKey:this._signKey,deviceId:this.deviceId,vin:vehicle.vin,operationPassword:ep,language:this.config.language})),...this._authHeaders()};
76
+ await this.http.post('/carownerservice/oversea/vehicle/v1/operPwd/verify',`operatePassword=${encodeURIComponent(ep)}&vin=${encodeURIComponent(vehicle.vin)}`,{headers:vh});
77
+ const h={...this._h(buildRemoteCtlWriteHeaders({signKey:this._signKey,deviceId:this.deviceId,vin:vehicle.vin,cmdContent,cmdId,operationPassword:ep,language:this.config.language})),...this._authHeaders()};
78
+ const body=`cmdContent=${encodeURIComponent(cmdContent)}&vin=${encodeURIComponent(vehicle.vin)}&cmdId=${encodeURIComponent(cmdId)}&operatePassword=${encodeURIComponent(ep)}`;
79
+ const resp=await this.http.post('/carownerservice/oversea/vehicle/v1/app/remote/ctl',body,{headers:h});
80
+ const result=this._parseResponse(resp.data,`remote ${cmdId}`);await this._pollResult(result);return result;
81
+ }
82
+
83
+ async getMileageEnergyDetail(vehicle){
84
+ const h={...this._h(require('./leapmotor-crypto').buildSignedHeaders({signKey:this._signKey,deviceId:this.deviceId,vin:vehicle.vin,language:this.config.language})),...this._authHeaders()};
85
+ const resp=await this.http.post('/carownerservice/oversea/drivingRecord/v1/mileage/energy/detail',`vin=${encodeURIComponent(vehicle.vin)}`,{headers:h});
86
+ return this._parseResponse(resp.data,'mileage energy');
87
+ }
88
+ async getConsumptionWeeklyRank(vehicle){
89
+ const crypto=require('crypto');
90
+ const n=String(Math.floor(Math.random()*9900000+100000));
91
+ const ts=String(Date.now());
92
+ const lang=this.config.language||'en-GB';
93
+ const vin=vehicle.vin;
94
+ // Spezielle Signatur: language+carvin+channel+deviceId+deviceType+nonce+source+timestamp+version
95
+ const si=[lang,vin,'1',this.deviceId,'1',n,'leapmotor',ts,'1.12.3'].join('');
96
+ const sign=crypto.createHmac('sha256',this._signKey).update(si).digest('hex');
97
+ const h={...this._authHeaders(),nonce:n,deviceId:this.deviceId,timestamp:ts,sign,acceptLanguage:lang,channel:'1',source:'leapmotor',deviceType:'1',version:'1.12.3'};
98
+ const resp=await this.http.post('/carownerservice/oversea/drivingRecord/v1/getLastNweeks100kmECAndRank',`carvin=${encodeURIComponent(vin)}`,{headers:h});
99
+ return this._parseResponse(resp.data,'consumption weekly');
100
+ }
101
+ async getCarPictureKey(vehicle){
102
+ const crypto=require('crypto');
103
+ const n=String(Math.floor(Math.random()*9900000+100000));
104
+ const ts=String(Date.now());
105
+ const lang=this.config.language||'en-GB';
106
+ // Spezielle Signatur: language+channel+deviceId+deviceId+deviceType+nonce+source+timestamp+version+vin
107
+ const si=[lang,'1',this.deviceId,this.deviceId,'1',n,'leapmotor',ts,'1.12.3',vehicle.vin].join('');
108
+ const sign=crypto.createHmac('sha256',this._signKey).update(si).digest('hex');
109
+ const h={...this._authHeaders(),nonce:n,deviceId:this.deviceId,timestamp:ts,sign,acceptLanguage:lang,channel:'1',source:'leapmotor',deviceType:'1',version:'1.12.3'};
110
+ const body=`deviceID=${encodeURIComponent(this.deviceId)}&vin=${encodeURIComponent(vehicle.vin)}`;
111
+ const resp=await this.http.post('/carownerservice/oversea/vehicle/v1/carpicture/key',body,{headers:h});
112
+ return this._parseResponse(resp.data,'car picture key');
113
+ }
114
+ async downloadCarPictureZip(key){
115
+ const crypto=require('crypto');
116
+ const https=require('https');
117
+ const n=String(Math.floor(Math.random()*9900000+100000));
118
+ const ts=String(Date.now());
119
+ const lang=this.config.language||'en-GB';
120
+ const si=[lang,'1',this.deviceId,'1',key,n,'leapmotor',ts,'1.12.3'].join('');
121
+ const sign=crypto.createHmac('sha256',this._signKey).update(si).digest('hex');
122
+ const h={...this._authHeaders(),nonce:n,deviceId:this.deviceId,timestamp:ts,sign,
123
+ acceptLanguage:lang,channel:'1',source:'leapmotor',deviceType:'1',version:'1.12.3',
124
+ 'Content-Type':'application/x-www-form-urlencoded'};
125
+ const body=`key=${encodeURIComponent(key)}`;
126
+ const certPem=this.accountCertPem;
127
+ const keyPem=this.accountKeyPem;
128
+ return new Promise((resolve,reject)=>{
129
+ const agent=new https.Agent({cert:certPem,key:keyPem,rejectUnauthorized:false});
130
+ const req=https.request({
131
+ hostname:'appgateway.leapmotor-international.de',
132
+ path:'/carownerservice/oversea/vehicle/v1/carpicture/package',
133
+ method:'POST',
134
+ agent,
135
+ headers:{...h,'Content-Length':Buffer.byteLength(body)},
136
+ timeout:60000
137
+ },(res)=>{
138
+ const chunks=[];
139
+ res.on('data',c=>chunks.push(c));
140
+ res.on('end',()=>resolve(Buffer.concat(chunks)));
141
+ res.on('error',reject);
142
+ });
143
+ req.on('error',reject);
144
+ req.on('timeout',()=>{req.destroy();reject(new Error('timeout'))});
145
+ req.write(body);
146
+ req.end();
147
+ });
148
+ }
149
+ async _pollResult(result){
150
+ const rd=result.data||{};const id=String(rd.remoteCtlId||'');if(!id)return;
151
+ const deadline=Date.now()+Math.max(Number(rd.queryRemoteCtlResultTimeout||30000),1000);
152
+ const interval=Number(rd.queryInterval||2000);
153
+ while(Date.now()<deadline){
154
+ await new Promise(r=>setTimeout(r,interval));
155
+ const h={...this._h(buildRemoteCtlResultHeaders({signKey:this._signKey,deviceId:this.deviceId,remoteCtlId:id,language:this.config.language})),...this._authHeaders()};
156
+ const resp=await this.http.post('/carownerservice/oversea/vehicle/v1/app/remote/ctl/result/query',`remoteCtlId=${encodeURIComponent(id)}`,{headers:h});
157
+ if(this._parseResponse(resp.data,'remote poll').data===1)return;
158
+ }
159
+ }
160
+ }
161
+ exports.LeapmotorClient=LeapmotorClient;
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+ Object.defineProperty(exports,"__esModule",{value:true});
3
+ const crypto=require('crypto');
4
+ const SM4_SBOX=new Uint8Array([0xD6,0x90,0xE9,0xFE,0xCC,0xE1,0x3D,0xB7,0x16,0xB6,0x14,0xC2,0x28,0xFB,0x2C,0x05,0x2B,0x67,0x9A,0x76,0x2A,0xBE,0x04,0xC3,0xAA,0x44,0x13,0x26,0x49,0x86,0x06,0x99,0x9C,0x42,0x50,0xF4,0x91,0xEF,0x98,0x7A,0x33,0x54,0x0B,0x43,0xED,0xCF,0xAC,0x62,0xE4,0xB3,0x1C,0xA9,0xC9,0x08,0xE8,0x95,0x80,0xDF,0x94,0xFA,0x75,0x8F,0x3F,0xA6,0x47,0x07,0xA7,0xFC,0xF3,0x73,0x17,0xBA,0x83,0x59,0x3C,0x19,0xE6,0x85,0x4F,0xA8,0x68,0x6B,0x81,0xB2,0x71,0x64,0xDA,0x8B,0xF8,0xEB,0x0F,0x4B,0x70,0x56,0x9D,0x35,0x1E,0x24,0x0E,0x5E,0x63,0x58,0xD1,0xA2,0x25,0x22,0x7C,0x3B,0x01,0x21,0x78,0x87,0xD4,0x00,0x46,0x57,0x9F,0xD3,0x27,0x52,0x4C,0x36,0x02,0xE7,0xA0,0xC4,0xC8,0x9E,0xEA,0xBF,0x8A,0xD2,0x40,0xC7,0x38,0xB5,0xA3,0xF7,0xF2,0xCE,0xF9,0x61,0x15,0xA1,0xE0,0xAE,0x5D,0xA4,0x9B,0x34,0x1A,0x55,0xAD,0x93,0x32,0x30,0xF5,0x8C,0xB1,0xE3,0x1D,0xF6,0xE2,0x2E,0x82,0x66,0xCA,0x60,0xC0,0x29,0x23,0xAB,0x0D,0x53,0x4E,0x6F,0xD5,0xDB,0x37,0x45,0xDE,0xFD,0x8E,0x2F,0x03,0xFF,0x6A,0x72,0x6D,0x6C,0x5B,0x51,0x8D,0x1B,0xAF,0x92,0xBB,0xDD,0xBC,0x7F,0x11,0xD9,0x5C,0x41,0x1F,0x10,0x5A,0xD8,0x0A,0xC1,0x31,0x88,0xA5,0xCD,0x7B,0xBD,0x2D,0x74,0xD0,0x12,0xB8,0xE5,0xB4,0xB0,0x89,0x69,0x97,0x4A,0x0C,0x96,0x77,0x7E,0x65,0xB9,0xF1,0x09,0xC5,0x6E,0xC6,0x84,0x18,0xF0,0x7D,0xEC,0x3A,0xDC,0x4D,0x20,0x79,0xEE,0x5F,0x3E,0xD7,0xCB,0x39,0x48]);
5
+ const RK=new Uint32Array([0x818FA553,0xEBA3318D,0x5FC3C93A,0xBD1DADD9,0xBB61CAB9,0x000FD7EA,0xDC6E0166,0xDA937279,0x607EE786,0xB548754C,0x107330E4,0xEA17C186,0x0F56F74B,0xB21E443C,0xE1210FE2,0x009995C8,0xE7529A48,0x6EF474F6,0x2AB06DF6,0x43B11BE8,0x359D4A14,0xC29E2CDE,0x30CF6A3E,0x79D1C806,0x7C502387,0xAAAB9BC6,0xF0FE744B,0x1CAFC872,0x95A9D075,0x88070D58,0x22800475,0x8391938B]);
6
+ function rl(v,b){return((v<<b)|(v>>>(32-b)))>>>0}
7
+ function sm4Block(buf){let x0=buf.readUInt32BE(0),x1=buf.readUInt32BE(4),x2=buf.readUInt32BE(8),x3=buf.readUInt32BE(12);for(let i=0;i<32;i++){const t=(x1^x2^x3^RK[i])>>>0;const b=((SM4_SBOX[(t>>>24)&0xFF]<<24)|(SM4_SBOX[(t>>>16)&0xFF]<<16)|(SM4_SBOX[(t>>>8)&0xFF]<<8)|SM4_SBOX[t&0xFF])>>>0;const n=(x0^b^rl(b,2)^rl(b,10)^rl(b,18)^rl(b,24))>>>0;x0=x1;x1=x2;x2=x3;x3=n}const r=Buffer.alloc(16);r.writeUInt32BE(x3,0);r.writeUInt32BE(x2,4);r.writeUInt32BE(x1,8);r.writeUInt32BE(x0,12);return r}
8
+ function p12Encode(data){const p=16-(data.length%16);const pad=Buffer.alloc(data.length+p);data.copy(pad);pad.fill(p,data.length);const res=Buffer.alloc(pad.length);for(let i=0;i<pad.length/16;i++)sm4Block(pad.slice(i*16,(i+1)*16)).copy(res,i*16);return res}
9
+ function deriveAccountP12Password(id,uid){const cn=crypto.createHash('md5').update(String(id)).digest('hex');const even=cn.split('').filter((_,i)=>i%2===0).join('');const odd=uid.split('').filter((_,i)=>i%2===1).join('');const digest=crypto.createHash('sha256').update(cn+even+odd,'ascii').digest();return p12Encode(digest).slice(0,12).toString('base64').slice(0,15)}
10
+ function deriveSignKey(ikm,salt,info){return Buffer.from(crypto.hkdfSync('sha256',Buffer.from(ikm),Buffer.from(salt),Buffer.from(info),32))}
11
+ function deriveOperpwdKeyIv(token){if(!token||token.length<64)return['defaultkeydefault','defaultivdefault!'];const k=crypto.createHash('md5').update(token.slice(0,32)).digest('hex').slice(8,24);const v=crypto.createHash('md5').update(token.slice(32,64)).digest('hex').slice(8,24);return[k,v]}
12
+ function encryptOperatePassword(pin,token){const[k,v]=deriveOperpwdKeyIv(token);const c=crypto.createCipheriv('aes-128-cbc',Buffer.from(k),Buffer.from(v));return Buffer.concat([c.update(Buffer.from(pin)),c.final()]).toString('base64')}
13
+ function deriveSessionDeviceId(token,fallback){if(!token)return fallback;try{const p=token.split('.');if(p.length<2)return fallback;const pl=JSON.parse(Buffer.from(p[1],'base64').toString('utf8'));const un=String(pl?.user_name??'');const s=un.split(',');if(s.length>=4&&s[2])return s[2]}catch{}return fallback}
14
+ const VER='1.12.3',CH='1',DT='1',SRC='leapmotor';
15
+ function nonce(){return String(Math.floor(Math.random()*9900000+100000))}
16
+ function buildLoginHeaders({deviceId,username,password,language='en-GB'}){const n=nonce(),ts=String(Date.now());const si=[language,DT,deviceId,'1',username,'0','1',n,password,'20260204',SRC,ts,VER].join('');return{nonce:n,deviceId,timestamp:ts,sign:crypto.createHash('sha256').update(si).digest('hex'),acceptLanguage:language}}
17
+ function buildSignedHeaders({signKey,deviceId,vin,language='en-GB',bodyParams}){const n=nonce(),ts=String(Date.now());const f={acceptLanguage:language,channel:CH,deviceId,deviceType:DT,nonce:n,source:SRC,timestamp:ts,version:VER};if(vin)f.vin=vin;if(bodyParams)Object.assign(f,bodyParams);const si=Object.keys(f).sort().map(k=>f[k]).join('');return{nonce:n,deviceId,timestamp:ts,sign:crypto.createHmac('sha256',signKey).update(si).digest('hex'),acceptLanguage:language}}
18
+ function buildOperpwdVerifyHeaders({signKey,deviceId,vin,operationPassword,language='en-GB'}){const n=nonce(),ts=String(Date.now());const si=[language,CH,deviceId,DT,n,operationPassword,SRC,ts,VER,vin].join('');return{nonce:n,deviceId,timestamp:ts,sign:crypto.createHmac('sha256',signKey).update(si).digest('hex'),acceptLanguage:language}}
19
+ function buildRemoteCtlWriteHeadersWithoutPin({signKey,deviceId,vin,cmdContent,cmdId,language='en-GB'}){const n=nonce(),ts=String(Date.now());const si=[language,CH,cmdContent,cmdId,deviceId,DT,n,SRC,ts,VER,vin].join('');return{nonce:n,deviceId,timestamp:ts,sign:crypto.createHmac('sha256',signKey).update(si).digest('hex'),acceptLanguage:language}}
20
+ function buildRemoteCtlWriteHeaders({signKey,deviceId,vin,cmdContent,cmdId,operationPassword,language='en-GB'}){const n=nonce(),ts=String(Date.now());const si=[language,CH,cmdContent,cmdId,deviceId,DT,n,operationPassword,SRC,ts,VER,vin].join('');return{nonce:n,deviceId,timestamp:ts,sign:crypto.createHmac('sha256',signKey).update(si).digest('hex'),acceptLanguage:language}}
21
+ function buildRemoteCtlResultHeaders({signKey,deviceId,remoteCtlId,language='en-GB'}){const n=nonce(),ts=String(Date.now());const si=[language,CH,deviceId,DT,n,remoteCtlId,SRC,ts,VER].join('');return{nonce:n,deviceId,timestamp:ts,sign:crypto.createHmac('sha256',signKey).update(si).digest('hex'),acceptLanguage:language}}
22
+ exports.deriveAccountP12Password=deriveAccountP12Password;
23
+ exports.deriveSignKey=deriveSignKey;
24
+ exports.encryptOperatePassword=encryptOperatePassword;
25
+ exports.deriveSessionDeviceId=deriveSessionDeviceId;
26
+ exports.buildLoginHeaders=buildLoginHeaders;
27
+ exports.buildSignedHeaders=buildSignedHeaders;
28
+ exports.buildOperpwdVerifyHeaders=buildOperpwdVerifyHeaders;
29
+ exports.buildRemoteCtlWriteHeadersWithoutPin=buildRemoteCtlWriteHeadersWithoutPin;
30
+ exports.buildRemoteCtlWriteHeaders=buildRemoteCtlWriteHeaders;
31
+ exports.buildRemoteCtlResultHeaders=buildRemoteCtlResultHeaders;
package/build/main.js ADDED
@@ -0,0 +1,405 @@
1
+ 'use strict';
2
+ const utils=require('@iobroker/adapter-core');
3
+ const {LeapmotorClient}=require('./leapmotor-client');
4
+ const fs=require('fs');
5
+ const path=require('path');
6
+ const CERT_DIR=path.join(__dirname,'..','certs');
7
+ const PICTURE_CACHE=path.join(__dirname,'..','pictures_cache.json');
8
+
9
+ // ── Farben für composite_html ────────────────────────────────
10
+ const C={
11
+ bg:'#070d1a', bg2:'#0d1520', border:'#1e2d45',
12
+ text:'#c8ddf0', textDim:'#2a4060',
13
+ accent:'#00d4ff', green:'#00ff88', yellow:'#ffcc00',
14
+ red:'#ff4444', orange:'#ff9900',
15
+ heat:'#ff6644', cool:'#00d4ff', vent:'#7c6aff',
16
+ carBg1:'#111f35', carBg2:'#070d1a',
17
+ };
18
+
19
+ class LeapmotorAdapter extends utils.Adapter{
20
+ constructor(options={}){
21
+ super({...options,name:'leapmotor'});
22
+ this.client=null;this.vehicles=[];this.pollTimer=null;this.isPolling=false;
23
+ this.on('ready',this.onReady.bind(this));
24
+ this.on('stateChange',this.onStateChange.bind(this));
25
+ this.on('unload',this.onUnload.bind(this));
26
+ }
27
+
28
+ async onReady(){
29
+ this.setState('info.connection',false,true);
30
+ const cfg=this.config;
31
+ if(!cfg.email||!cfg.password){this.log.error('E-Mail und Passwort müssen konfiguriert sein!');return}
32
+ let appCertPem,appKeyPem;
33
+ try{appCertPem=fs.readFileSync(path.join(CERT_DIR,'app.crt'),'utf8');appKeyPem=fs.readFileSync(path.join(CERT_DIR,'app.key'),'utf8')}
34
+ catch(e){this.log.error(`Zertifikate nicht gefunden: ${e}`);return}
35
+ this.client=new LeapmotorClient({username:cfg.email,password:cfg.password,appCertPem,appKeyPem,operationPassword:cfg.operationPassword||undefined,language:cfg.language||'en-GB'});
36
+ try{this.log.info('Verbinde mit Leapmotor Cloud...');await this.client.login();this.log.info('Login erfolgreich.');this.setState('info.connection',true,true)}
37
+ catch(e){this.log.error(`Login fehlgeschlagen: ${e}`);return}
38
+ try{
39
+ this.vehicles=await this.client.getVehicleList();
40
+ this.log.info(`${this.vehicles.length} Fahrzeug(e) gefunden.`);
41
+ for(const v of this.vehicles){this.log.info(` → ${v.name} (${v.carType}) VIN: ${v.vin}`);await this.createVehicleObjects(v);await this.subscribeStatesAsync(`${v.vin}.cmd.*`)}
42
+ }catch(e){this.log.error(`Fahrzeugliste fehlgeschlagen: ${e}`);return}
43
+ await this.pollAll();
44
+ for(const v of this.vehicles)await this.updatePictures(v);
45
+ const interval=Math.max(1,cfg.pollingInterval||5)*60*1000;
46
+ this.pollTimer=setInterval(()=>this.pollAll(),interval);
47
+ this.log.info(`Polling alle ${cfg.pollingInterval||5} Minuten.`);
48
+ }
49
+
50
+ async pollAll(){
51
+ if(this.isPolling||!this.client)return;
52
+ this.isPolling=true;
53
+ try{
54
+ for(const v of this.vehicles)await this.updateVehicleStatus(v);
55
+ }catch(e){
56
+ const msg=String(e);
57
+ if(msg.includes('ungültig')||msg.includes('Token')||msg.includes('401')){
58
+ this.log.debug('Token abgelaufen – re-login...');
59
+ try{await this.client.login();this.log.debug('Re-Login erfolgreich.');for(const v of this.vehicles)await this.updateVehicleStatus(v);}
60
+ catch(e2){this.log.error('Re-Login fehlgeschlagen: '+e2);}
61
+ }else{this.log.warn('Polling Fehler: '+e);}
62
+ }finally{this.isPolling=false;}
63
+ }
64
+
65
+ async updateVehicleStatus(vehicle){
66
+ if(!this.client)return;
67
+ try{
68
+ const s=await this.client.getVehicleStatus(vehicle);
69
+ await this.writeStatusStates(vehicle.vin,s);
70
+ this.log.debug(`${vehicle.vin}: SOC=${s.soc}% Range=${s.expectedMileage}km`);
71
+ await this.buildCompositeHtml(vehicle.vin,s);
72
+ }catch(e){
73
+ const msg=String(e);
74
+ if(msg.includes('ungültig')||msg.includes('Token')||msg.includes('401')){throw e;}
75
+ this.log.warn('Status Fehler '+vehicle.vin+': '+e);
76
+ }
77
+ try{await this.updateConsumption(vehicle)}catch(e){this.log.warn(`Verbrauch Fehler: ${e}`)}
78
+ }
79
+
80
+ async updateConsumption(vehicle){
81
+ if(!this.client)return;
82
+ const vin=vehicle.vin;
83
+ try{
84
+ const m=await this.client.getMileageEnergyDetail(vehicle);
85
+ const d=m.data||{};
86
+ await this.ensureAndSet(`${vin}.consumption.mileage_total_km`,d.totalmileage,'number','km','Gesamtkilometer');
87
+ await this.ensureAndSet(`${vin}.consumption.delivery_days`,d.deliveryDays,'number','Tage','Liefertage');
88
+ }catch(e){this.log.warn('Mileage Fehler: '+e)}
89
+ try{
90
+ const w=await this.client.getConsumptionWeeklyRank(vehicle);
91
+ const d=w.data||{};
92
+ const rank=d.rankResult||d.rank||{};
93
+ await this.ensureAndSet(`${vin}.consumption.kwh_100km`,rank.hundredKmEC||rank.hundredKmEc||rank.hundred_km_ec,'number','kWh/100km','Ø Verbrauch');
94
+ await this.ensureAndSet(`${vin}.consumption.rank`,rank.rank,'string','','Ranking');
95
+ const weekly=d.weeklyEC||d.weekly||[];
96
+ for(let i=0;i<weekly.length;i++){
97
+ const wb=`${vin}.consumption.week_${i+1}`;
98
+ await this.ensureAndSet(`${wb}.week_start`,weekly[i].weekStart||weekly[i].week_start,'string','','Start');
99
+ await this.ensureAndSet(`${wb}.week_end`,weekly[i].weekEnd||weekly[i].week_end,'string','','Ende');
100
+ await this.ensureAndSet(`${wb}.kwh_100km`,weekly[i].hundredKmEC||weekly[i].hundredKmEc||weekly[i].hundred_km_ec,'number','kWh/100km','Verbrauch');
101
+ }
102
+ }catch(e){this.log.warn('Weekly Fehler: '+e)}
103
+ }
104
+
105
+ async ensureAndSet(id,val,type,unit,name){
106
+ if(val===null||val===undefined)return;
107
+ await this.setObjectNotExistsAsync(id,{type:'state',common:{name,type,unit,role:'value',read:true,write:false},native:{}});
108
+ await this.setStateAsync(id,{val,ack:true});
109
+ }
110
+
111
+ // ── Fahrzeugbilder ───────────────────────────────────────
112
+
113
+ async updatePictures(vehicle){
114
+ if(!this.client)return;
115
+ const vin=vehicle.vin;
116
+ let cache={};
117
+ try{if(fs.existsSync(PICTURE_CACHE))cache=JSON.parse(fs.readFileSync(PICTURE_CACHE,'utf8'))}catch{}
118
+ if(cache[vin]&&Object.keys(cache[vin]).length>0){
119
+ this.log.info(`${vin}: Bilder aus Cache (${Object.keys(cache[vin]).length} Bilder)`);
120
+ await this.writePictures(vin,cache[vin]);
121
+ return;
122
+ }
123
+ try{
124
+ const keyResp=await this.client.getCarPictureKey(vehicle);
125
+ const key=(keyResp.data||{}).key;
126
+ if(!key){this.log.warn('Kein Bild-Key');return}
127
+ this.log.info(`${vin}: Lade Fahrzeugbilder...`);
128
+ const zipBuf=await this.client.downloadCarPictureZip(key);
129
+ const AdmZip=require('adm-zip');
130
+ const zip=new AdmZip(zipBuf);
131
+ const pics={};
132
+ zip.getEntries().forEach(e=>{
133
+ if(e.entryName.startsWith('android/xxhdpi/')&&e.entryName.endsWith('.png')){
134
+ const name=e.entryName.split('/').pop().replace('.png','');
135
+ pics[name]='data:image/png;base64,'+e.getData().toString('base64');
136
+ }
137
+ });
138
+ cache[vin]=pics;
139
+ fs.writeFileSync(PICTURE_CACHE,JSON.stringify(cache));
140
+ this.log.info(`${vin}: ${Object.keys(pics).length} Bilder gecacht`);
141
+ await this.writePictures(vin,pics);
142
+ }catch(e){this.log.warn('Bilder Fehler: '+e);}
143
+ }
144
+
145
+ async writePictures(vin,pics){
146
+ for(const[name,data]of Object.entries(pics)){
147
+ await this.ensureAndSet(`${vin}.pictures.${name}`,data,'string','','Bild: '+name);
148
+ }
149
+ }
150
+
151
+ // ── Composite HTML ───────────────────────────────────────
152
+
153
+ async buildCompositeHtml(vin,s){
154
+ // Bilder aus Cache lesen
155
+ let pics={};
156
+ try{
157
+ let cache={};
158
+ if(fs.existsSync(PICTURE_CACHE))cache=JSON.parse(fs.readFileSync(PICTURE_CACHE,'utf8'));
159
+ pics=cache[vin]||{};
160
+ }catch{}
161
+ if(!pics['carpic_for_tripsum']&&!pics['carpic_body'])return;
162
+
163
+ const soc=s.soc||0;
164
+ const socColor=soc>50?C.green:soc>20?C.yellow:C.red;
165
+ const anyDoor=s.lbcmDriverDoorStatus||s.rbcmDriverDoorStatus||s.lbcmLeftRearDoorStatus||s.rbcmRightRearDoorStatus;
166
+ const anyOpen=anyDoor||s.bbcmBackDoorStatus;
167
+ const windowOpen=(s.leftFrontWindowPercent>0)||(s.rightFrontWindowPercent>0)||(s.leftRearWindowPercent>0)||(s.rightRearWindowPercent>0);
168
+ const charging=s.chargeState!=null&&[1,2,3].includes(s.chargeState);
169
+ const plugged=s.chargeState>0;
170
+
171
+ // Bild-Layer
172
+ const lay='position:absolute;top:0;left:0;width:100%;height:100%;object-fit:contain;';
173
+ const layers=[];
174
+ if(!anyOpen&&!charging&&!plugged){
175
+ layers.push(pics['carpic_for_tripsum']||'');
176
+ }else{
177
+ layers.push(pics['carpic_body']||'');
178
+ layers.push(pics['carpic_hood_close']||'');
179
+ layers.push(s.lbcmDriverDoorStatus?(pics['carpic_leftfront_open']||''):(pics['carpic_leftfront_close']||''));
180
+ layers.push(s.lbcmLeftRearDoorStatus?(pics['carpic_leftbehind_open']||''):(pics['carpic_leftbehind_close']||''));
181
+ if(s.rbcmDriverDoorStatus)layers.push(pics['carpic_rightfront_open']||'');
182
+ if(s.rbcmRightRearDoorStatus)layers.push(pics['carpic_rightbehind_open']||'');
183
+ if(s.bbcmBackDoorStatus)layers.push(pics['carpic_tailgate_open']||'');
184
+ if(plugged||charging)layers.push(pics['carpic_charge_open']||'');
185
+ }
186
+
187
+ let imgTags=layers.filter(Boolean).map(src=>`<img src="${src}" style="${lay}">`).join('');
188
+
189
+ // Ladeanimation CSS
190
+ if(charging){
191
+ const n=15,dur=0.12,total=(n*dur).toFixed(2);
192
+ const pOn=(1/n*100).toFixed(1),pOff=(2/n*100).toFixed(1);
193
+ let css='',fImgs='';
194
+ for(let i=0;i<n;i++){
195
+ const src=pics[`carpic_charge${i+1}`]||'';if(!src)continue;
196
+ const a=`chf${i}`,d=(i*dur).toFixed(2);
197
+ css+=`@keyframes ${a}{0%{opacity:0}${pOn}%{opacity:1}${pOff}%{opacity:0}100%{opacity:0}}`;
198
+ fImgs+=`<img src="${src}" style="${lay}opacity:0;animation:${a} ${total}s ${d}s infinite;">`;
199
+ }
200
+ imgTags+=`<style>${css}</style>${fImgs}`;
201
+ }
202
+
203
+ // Badges
204
+ const locked=s.driverDoorLockStatus;
205
+ const acOn=s.acSwitch;
206
+ const badges=[
207
+ locked?`<span style="background:${C.accent}15;border:1px solid ${C.accent}33;border-radius:5px;padding:2px 7px;font-size:9px;color:${C.accent};font-family:monospace">🔒 GESPERRT</span>`:'',
208
+ charging?`<span style="background:${C.green}15;border:1px solid ${C.green}33;border-radius:5px;padding:2px 7px;font-size:9px;color:${C.green};font-family:monospace">⚡ LÄDT</span>`:'',
209
+ acOn?`<span style="background:${C.vent}15;border:1px solid ${C.vent}33;border-radius:5px;padding:2px 7px;font-size:9px;color:${C.vent};font-family:monospace">❄ KLIMA</span>`:'',
210
+ windowOpen?`<span style="background:${C.orange}15;border:1px solid ${C.orange}33;border-radius:5px;padding:2px 7px;font-size:9px;color:${C.orange};font-family:monospace">🪟 FENSTER</span>`:'',
211
+ ].filter(Boolean).join('');
212
+
213
+ // Temp
214
+ const temp=(s.outdoorTemp||0)+'&deg;C';
215
+ const range=(s.expectedMileage||0)+' km';
216
+
217
+ // Status-Kacheln
218
+ const tile=(label,val,color)=>`<div style="background:${C.bg2};border:1px solid ${C.border};border-radius:10px;padding:10px;text-align:center"><div style="font-size:8px;color:${C.textDim};letter-spacing:0.1em;margin-bottom:3px">${label}</div><div style="font-size:12px;font-weight:800;color:${color||C.text}">${val}</div></div>`;
219
+
220
+ const tiles=`<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-bottom:8px">
221
+ ${tile('AUSSEN',(s.outdoorTemp||0)+'°C',C.text)}
222
+ ${tile('REICHWEITE',(s.expectedMileage||0)+' km',C.accent)}
223
+ ${tile('STATUS',s.speed===0?'🅿 Geparkt':'▶ '+(s.speed||0)+' km/h',s.speed===0?C.green:C.yellow)}
224
+ ${tile('LADEN',charging?'⚡ '+(s.chargeRemainTime||0)+' min':'— —',charging?C.green:C.textDim)}
225
+ ${tile('TÜREN',anyOpen?'🚪 Offen':'✓ Zu',anyOpen?C.red:C.green)}
226
+ ${tile('SCHLOSS',locked?'🔒 Zu':'🔓 Offen',locked?C.accent:C.red)}
227
+ </div>`;
228
+
229
+ // Klima-Buttons (servConn.setState für VIS)
230
+ const ns=`leapmotor.${this.instance}.${vin}`;
231
+ const sdp=(dp,val)=>`servConn.setState('${ns}.${dp}',${val})`;
232
+ const acMode=acOn?(s.acSetting>=23?'kuehl':(s.acSetting<20?'heiz':'luft')):'';
233
+ const bs=(color,active)=>`background:${active?color+'22':C.bg2};border:1px solid ${active?color+'55':C.border};border-radius:10px;padding:10px 4px;color:${active?color:C.textDim};font-size:10px;font-weight:700;cursor:pointer;font-family:monospace;display:flex;flex-direction:column;align-items:center;gap:3px;width:100%`;
234
+
235
+ const acTemp=(()=>{try{return 22;}catch{return 22;}})();
236
+
237
+ const climateRow=`<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:6px;margin-bottom:8px">
238
+ <button style="${bs(C.heat,acMode==='heiz')}" onclick="${sdp('cmd.ac_heiz','true')}"><span style="font-size:16px">🔥</span>Heizung</button>
239
+ <button style="${bs(C.cool,acMode==='kuehl')}" onclick="${sdp('cmd.ac_kuehl','true')}"><span style="font-size:16px">❄️</span>Kühlung</button>
240
+ <button style="${bs(C.vent,acMode==='luft')}" onclick="${sdp('cmd.ac_luft','true')}"><span style="font-size:16px">💨</span>Lüftung</button>
241
+ <button style="${bs(C.red,!acOn)}" onclick="${sdp('cmd.ac_off','true')}"><span style="font-size:16px">⏹</span>Aus</button>
242
+ </div>`;
243
+
244
+ const tempRow=`<div style="display:flex;align-items:center;justify-content:space-between;background:${C.bg2};border-radius:10px;padding:8px 12px;margin-bottom:8px;border:1px solid ${C.border}">
245
+ <span style="font-size:9px;color:${C.textDim};letter-spacing:0.12em;text-transform:uppercase">Zieltemperatur</span>
246
+ <div style="display:flex;align-items:center;gap:12px">
247
+ <button onclick="${sdp('cmd.ac_temp',Math.max(16,(s.acSetting||22)-1))}" style="background:${C.border};border:none;border-radius:6px;color:${C.text};font-size:18px;width:30px;height:30px;cursor:pointer;font-weight:700">−</button>
248
+ <span style="font-size:18px;font-weight:800;color:${C.accent};min-width:40px;text-align:center">${s.acSetting||22}°C</span>
249
+ <button onclick="${sdp('cmd.ac_temp',Math.min(30,(s.acSetting||22)+1))}" style="background:${C.border};border:none;border-radius:6px;color:${C.text};font-size:18px;width:30px;height:30px;cursor:pointer;font-weight:700">+</button>
250
+ </div>
251
+ </div>`;
252
+
253
+ const lockRow=`<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:6px">
254
+ <button style="${bs(C.accent,locked)}" onclick="${sdp('cmd.lock','true')}"><span style="font-size:16px">🔒</span>Sperren</button>
255
+ <button style="${bs(C.orange,!locked)}" onclick="${sdp('cmd.unlock','true')}"><span style="font-size:16px">🔓</span>Öffnen</button>
256
+ <button style="${bs(C.textDim,false)}" onclick="${sdp('cmd.refresh','true')}"><span style="font-size:16px">🔄</span>Refresh</button>
257
+ </div>`;
258
+
259
+ const html=`<div style="font-family:monospace;background:${C.bg};border-radius:18px;overflow:hidden;border:1px solid #1a2a40;box-shadow:0 24px 64px rgba(0,0,0,0.6)">
260
+ <div style="padding:12px 14px 0;display:flex;justify-content:space-between;align-items:flex-end">
261
+ <div>
262
+ <div style="font-size:9px;color:${C.textDim};letter-spacing:0.25em;text-transform:uppercase;margin-bottom:2px">LEAPMOTOR T03</div>
263
+ <div style="font-size:20px;font-weight:800;color:${C.text}">Kn&ouml;psel</div>
264
+ </div>
265
+ <div style="text-align:right">
266
+ <div style="font-size:9px;color:${C.textDim};letter-spacing:0.15em;margin-bottom:2px">AKKUSTAND</div>
267
+ <div style="font-size:16px;font-weight:700;color:${socColor}">${soc}%</div>
268
+ </div>
269
+ </div>
270
+ <div style="position:relative;width:100%;padding-bottom:46%;background:radial-gradient(ellipse at center,${C.carBg1} 0%,${C.carBg2} 70%)">
271
+ ${imgTags}
272
+ <div style="position:absolute;bottom:8px;left:10px;display:flex;gap:5px">${badges}</div>
273
+ <div style="position:absolute;top:8px;right:10px;font-size:10px;color:${C.textDim}">${temp}</div>
274
+ </div>
275
+ <div style="padding:6px 14px 0">
276
+ <div style="background:${C.bg2};border-radius:3px;height:4px;overflow:hidden">
277
+ <div style="height:100%;width:${soc}%;background:linear-gradient(90deg,${socColor}88,${socColor});border-radius:3px"></div>
278
+ </div>
279
+ </div>
280
+ <div style="padding:10px 14px 14px">
281
+ ${tiles}${climateRow}${tempRow}${lockRow}
282
+ </div>
283
+ </div>`;
284
+
285
+ await this.ensureAndSet(`${vin}.pictures.composite_html`,html,'string','','Fahrzeug Dashboard HTML');
286
+ }
287
+
288
+ // ── Objekte anlegen ──────────────────────────────────────
289
+
290
+ async createVehicleObjects(vehicle){
291
+ await this.setObjectNotExistsAsync(vehicle.vin,{type:'device',common:{name:`${vehicle.name} (${vehicle.carType})`},native:{vin:vehicle.vin,carType:vehicle.carType}});
292
+ const states=[
293
+ ['status.battery_soc','Ladestand','number','value.battery','%'],
294
+ ['status.battery_current','Batteriestrom','number','value','A'],
295
+ ['status.battery_voltage','Batteriespannung','number','value.voltage','V'],
296
+ ['status.battery_energy_kwh','Energie verbleibend','number','value','kWh'],
297
+ ['status.range_km','Reichweite','number','value.distance','km'],
298
+ ['status.mileage_total','Gesamtkilometer','number','value.distance','km'],
299
+ ['status.temp_outdoor','Außentemperatur','number','value.temperature','°C'],
300
+ ['status.temp_battery_min','Min. Zellentemp.','number','value.temperature','°C'],
301
+ ['status.charging_active','Lädt gerade','boolean','indicator.charging',''],
302
+ ['status.charging_state','Ladestatus','number','value',''],
303
+ ['status.charging_soc_limit','Ladelimit','number','value','%'],
304
+ ['status.charging_remain_min','Restladezeit','number','value','min'],
305
+ ['status.charging_plugged','Kabel eingesteckt','boolean','indicator',''],
306
+ ['status.dc_fast_charge','DC Schnellladen','boolean','indicator',''],
307
+ ['status.ac_on','Klimaanlage an','boolean','indicator',''],
308
+ ['status.ac_temp','Klima Solltemp.','number','value.temperature','°C'],
309
+ ['status.drive_speed','Geschwindigkeit','number','value.speed','km/h'],
310
+ ['status.drive_parked','Geparkt','boolean','indicator',''],
311
+ ['status.gear','Gang','number','value',''],
312
+ ['status.security_locked','Verriegelt','boolean','indicator',''],
313
+ ['status.door_driver','Fahrertür offen','boolean','indicator.door',''],
314
+ ['status.door_front_right','Vordertür re offen','boolean','indicator.door',''],
315
+ ['status.door_rear_left','Hintertür li offen','boolean','indicator.door',''],
316
+ ['status.door_rear_right','Hintertür re offen','boolean','indicator.door',''],
317
+ ['status.door_trunk','Kofferraum offen','boolean','indicator.door',''],
318
+ ['status.window_fl_pct','Fenster vl','number','value','%'],
319
+ ['status.window_fr_pct','Fenster vr','number','value','%'],
320
+ ['status.window_rl_pct','Fenster hl','number','value','%'],
321
+ ['status.window_rr_pct','Fenster hr','number','value','%'],
322
+ ['status.location_lat','GPS Breitengrad','number','value.gps.latitude',''],
323
+ ['status.location_lon','GPS Längengrad','number','value.gps.longitude',''],
324
+ ['status.tire_fl','Reifendruck vl','number','value','bar'],
325
+ ['status.tire_fr','Reifendruck vr','number','value','bar'],
326
+ ['status.tire_rl','Reifendruck hl','number','value','bar'],
327
+ ['status.tire_rr','Reifendruck hr','number','value','bar'],
328
+ ['status.bluetooth_on','Bluetooth aktiv','boolean','indicator',''],
329
+ ['status.hotspot_on','Hotspot aktiv','boolean','indicator',''],
330
+ ['status.collect_time','Fahrzeug-Zeitstempel','string','text',''],
331
+ ];
332
+ for(const[id,name,type,role,unit]of states){const common={name,type,role,read:true,write:false};if(unit)common.unit=unit;await this.setObjectNotExistsAsync(`${vehicle.vin}.${id}`,{type:'state',common,native:{}})}
333
+ const cmds=['ac_kuehl','ac_heiz','ac_luft','ac_off','defrost','windows_open','windows_close','find','battery_preheat','battery_preheat_off','lock','unlock','trunk_open','trunk_close','refresh'];
334
+ for(const cmd of cmds)await this.setObjectNotExistsAsync(`${vehicle.vin}.cmd.${cmd}`,{type:'state',common:{name:cmd,type:'boolean',role:'button',read:true,write:true,def:false},native:{}});
335
+ await this.setObjectNotExistsAsync(`${vehicle.vin}.cmd.ac_temp`,{type:'state',common:{name:'Zieltemperatur',type:'number',role:'value.temperature',read:true,write:true,min:16,max:30,unit:'°C',def:22},native:{}});
336
+ }
337
+
338
+ // ── Status schreiben ─────────────────────────────────────
339
+
340
+ async writeStatusStates(vin,s){
341
+ const set=async(id,val)=>{if(val!==null&&val!==undefined)await this.setStateAsync(`${vin}.${id}`,{val,ack:true})};
342
+ const tire=v=>v!=null?Math.round(v)/100:null;
343
+ await set('status.battery_soc',s.soc);await set('status.battery_current',s.batteryCurrent);
344
+ await set('status.battery_voltage',s.batteryVoltage);
345
+ await set('status.battery_energy_kwh',s.dumpEnergy!=null?Math.round(s.dumpEnergy/100)/10:null);
346
+ await set('status.range_km',s.expectedMileage);await set('status.mileage_total',s.totalMileage);
347
+ await set('status.temp_outdoor',s.outdoorTemp);await set('status.temp_battery_min',s.minSingleTemp);
348
+ await set('status.charging_active',s.chargeState!=null?[1,2,3].includes(s.chargeState):null);
349
+ await set('status.charging_state',s.chargeState);await set('status.charging_soc_limit',s.chargesocSetting);
350
+ await set('status.charging_remain_min',s.chargeRemainTime);
351
+ await set('status.charging_plugged',s.chargeState!=null?s.chargeState>0:null);
352
+ await set('status.dc_fast_charge',s.dcInputFastCharge!=null?s.dcInputFastCharge===1:null);
353
+ await set('status.ac_on',s.acSwitch);await set('status.ac_temp',s.acSetting);
354
+ await set('status.drive_speed',s.speed);await set('status.drive_parked',s.speed!=null?s.speed===0:null);
355
+ await set('status.gear',s.gearStatus);await set('status.security_locked',s.driverDoorLockStatus);
356
+ await set('status.door_driver',s.lbcmDriverDoorStatus);await set('status.door_front_right',s.rbcmDriverDoorStatus);
357
+ await set('status.door_rear_left',s.lbcmLeftRearDoorStatus);await set('status.door_rear_right',s.rbcmRightRearDoorStatus);
358
+ await set('status.door_trunk',s.bbcmBackDoorStatus);
359
+ await set('status.window_fl_pct',s.leftFrontWindowPercent);await set('status.window_fr_pct',s.rightFrontWindowPercent);
360
+ await set('status.window_rl_pct',s.leftRearWindowPercent);await set('status.window_rr_pct',s.rightRearWindowPercent);
361
+ await set('status.location_lat',s.latitude);await set('status.location_lon',s.longitude);
362
+ await set('status.tire_fl',tire(s.leftFrontTirePressure));await set('status.tire_fr',tire(s.rightFrontTirePressure));
363
+ await set('status.tire_rl',tire(s.leftRearTirePressure));await set('status.tire_rr',tire(s.rightRearTirePressure));
364
+ await set('status.bluetooth_on',s.bluetoothState);await set('status.hotspot_on',s.hotspotState);
365
+ await set('status.collect_time',s.collectTime);
366
+ }
367
+
368
+ // ── Befehle ──────────────────────────────────────────────
369
+
370
+ async onStateChange(id,state){
371
+ if(!state||state.ack||!this.client)return;
372
+ const parts=id.replace(`${this.namespace}.`,'').split('.');
373
+ if(parts.length<3||parts[1]!=='cmd')return;
374
+ const vin=parts[0],cmd=parts[2];
375
+ const vehicle=this.vehicles.find(v=>v.vin===vin);if(!vehicle)return;
376
+ if(cmd==='ac_temp'){await this.setStateAsync(id,{val:state.val,ack:true});return}
377
+ if(cmd==='refresh'&&state.val===true){
378
+ await this.updateVehicleStatus(vehicle);
379
+ await this.setStateAsync(id,{val:false,ack:true});
380
+ return;
381
+ }
382
+ if(state.val===true){await this.executeCommand(vehicle,cmd);await this.setStateAsync(id,{val:false,ack:true})}
383
+ }
384
+
385
+ async executeCommand(vehicle,cmd){
386
+ if(!this.client)return;
387
+ this.log.info(`Befehl: ${cmd} für ${vehicle.vin}`);
388
+ const tempState=await this.getStateAsync(`${vehicle.vin}.cmd.ac_temp`);
389
+ const temp=String(tempState?.val??22);
390
+ const ac=(mode,op)=>JSON.stringify({circle:'out',mode,operate:op,position:'all',temperature:temp,windlevel:'3',wshld:'0'});
391
+ const noPinCmds={'find':['120','{}'],'windows_open':['230','{"value":"100"}'],'windows_close':['230','{"value":"0"}']};
392
+ const pinCmds={'ac_kuehl':['170',ac('cold','manual')],'ac_heiz':['170',ac('hot','manual')],'ac_luft':['170',ac('wind','manual')],'ac_off':['170',ac('wind','off')],'defrost':['170',ac('wind','manual')],'battery_preheat':['190','{"operate":"on"}'],'battery_preheat_off':['190','{"operate":"off"}'],'lock':['110','{}'],'unlock':['110','{"operate":"unlock"}'],'trunk_open':['130','{"operate":"open"}'],'trunk_close':['130','{"operate":"close"}']};
393
+ try{
394
+ if(noPinCmds[cmd])await this.client.sendCommandWithoutPin(vehicle,...noPinCmds[cmd]);
395
+ else if(pinCmds[cmd])await this.client.sendCommandWithPin(vehicle,...pinCmds[cmd]);
396
+ else{this.log.warn(`Unbekannter Befehl: ${cmd}`);return}
397
+ this.log.info(`${cmd} erfolgreich.`);
398
+ setTimeout(()=>this.updateVehicleStatus(vehicle),15000);
399
+ }catch(e){this.log.error(`Befehl ${cmd} fehlgeschlagen: ${e}`)}
400
+ }
401
+
402
+ onUnload(callback){if(this.pollTimer){clearInterval(this.pollTimer);this.pollTimer=null}this.setState('info.connection',false,true);callback()}
403
+ }
404
+ if(require.main!==module){module.exports=options=>new LeapmotorAdapter(options)}
405
+ else{new LeapmotorAdapter()}
package/certs/app.crt ADDED
@@ -0,0 +1,27 @@
1
+ Bag Attributes
2
+ localKeyID: 99 29 7B 58 C4 E3 43 9A 31 3D 79 F0 3B 35 7B B9 FA 8E F4 FF
3
+ subject=C = cn, ST = zhejiang, L = hangzhou, O = leapmotor, OU = carnet, title = app, CN = LeapmotorAppCrtCN, UID = 202403070959
4
+ issuer=C = cn, ST = zhejiang, L = hangzhou, O = leapmotor, OU = carnet, CN = AppSubCA
5
+ -----BEGIN CERTIFICATE-----
6
+ MIIDzzCCAregAwIBAgIGf/q2su7JMA0GCSqGSIb3DQEBCwUAMGsxCzAJBgNVBAYT
7
+ AmNuMREwDwYDVQQIDAh6aGVqaWFuZzERMA8GA1UEBwwIaGFuZ3pob3UxEjAQBgNV
8
+ BAoMCWxlYXBtb3RvcjEPMA0GA1UECwwGY2FybmV0MREwDwYDVQQDDAhBcHBTdWJD
9
+ QTAeFw0yNDAzMDcwMTU5MzlaFw0yOTAzMDYwMTU5MzlaMIGgMQswCQYDVQQGEwJj
10
+ bjERMA8GA1UECAwIemhlamlhbmcxETAPBgNVBAcMCGhhbmd6aG91MRIwEAYDVQQK
11
+ DAlsZWFwbW90b3IxDzANBgNVBAsMBmNhcm5ldDEMMAoGA1UEDAwDYXBwMRowGAYD
12
+ VQQDDBFMZWFwbW90b3JBcHBDcnRDTjEcMBoGCgmSJomT8ixkAQEMDDIwMjQwMzA3
13
+ MDk1OTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALBD+5Q/aW6zsFE1
14
+ x0zj4j3NyGae2s6M+weKdRLZ395+IX/HZ1S2z1GehiRw3SbNBA0RL0vSTJgBzMcu
15
+ KIu2jCHUz8dNIVfZwKJhRf5KpFOHSfn0GDIDNRyvqBgr+8PIsViJ1563w5KFTZ8E
16
+ 6J2DqpigFn4RMrpg35pDf9HrHEm491mBSJRbqNrd6GSMYRtXKoXZqRsyedMhQ7GD
17
+ tGmDBl4Tb6Wh4wZHzmGopYCWlQAunK3cvyj0cBGm/2OBYiQBbhmjHG/NwBg9++tX
18
+ jGcMAwEZHx5+e+UxCXjEemAKhL/AVKqqBFDB7bqqGIaJ0Qw5jUC1D68sYR8/5cqF
19
+ BY4IRCECAwEAAaNDMEEwCQYDVR0TBAIwADALBgNVHQ8EBAMCAb4wJwYDVR0lBCAw
20
+ HgYIKwYBBQUHAwEGCCsGAQUFBwMCBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOC
21
+ AQEAR8h5ed4iFcpuhqsnUbA2OcUZWQoMsatH9FPuZ3xHbjVDzXjrwO2zccYbtOAJ
22
+ jtr/DOPZENEJLjLVNb7NqZb9j+GYAS6UPFK9EZTEXuS7HXFrg/vPpuGDtqjBZhZP
23
+ eEkDow0JwWhDXBh2/nGCVw2pc3JwV65RAWEc5CvyANa2dZJHsDPWfy+7nT9FcO8u
24
+ 6f4xE5SWcQDTH6LeCteoywqVRCSKt1Id3egEKG49dgQJ33mFRWL/K+PmA/kEyBVb
25
+ HMvloZREcwId6Usezx0wJp/H0Cjgno5exKh4lwn+Gjz7doV5TOBL8qP/c/cN3JiB
26
+ rJKzt1FC4/aDUMvEr8ed2EsFhg==
27
+ -----END CERTIFICATE-----
package/certs/app.key ADDED
@@ -0,0 +1,31 @@
1
+ Bag Attributes
2
+ localKeyID: 99 29 7B 58 C4 E3 43 9A 31 3D 79 F0 3B 35 7B B9 FA 8E F4 FF
3
+ Key Attributes: <No Attributes>
4
+ -----BEGIN PRIVATE KEY-----
5
+ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCwQ/uUP2lus7BR
6
+ NcdM4+I9zchmntrOjPsHinUS2d/efiF/x2dUts9RnoYkcN0mzQQNES9L0kyYAczH
7
+ LiiLtowh1M/HTSFX2cCiYUX+SqRTh0n59BgyAzUcr6gYK/vDyLFYideet8OShU2f
8
+ BOidg6qYoBZ+ETK6YN+aQ3/R6xxJuPdZgUiUW6ja3ehkjGEbVyqF2akbMnnTIUOx
9
+ g7RpgwZeE2+loeMGR85hqKWAlpUALpyt3L8o9HARpv9jgWIkAW4ZoxxvzcAYPfvr
10
+ V4xnDAMBGR8efnvlMQl4xHpgCoS/wFSqqgRQwe26qhiGidEMOY1AtQ+vLGEfP+XK
11
+ hQWOCEQhAgMBAAECggEAHLYGHaCymMCWILRE0WZxLKX/VY/cjjZykUjhRv/MMl6T
12
+ MFAXqbnZTS8oJHlp3G8akNJSxOPta/knFO6U1iUW8E/MxGbw3nFuqkRz9TbNS3nG
13
+ 9/rlkgfpt9F47O8FJF/lW0mgoI7IQW6LNTLEfRu8Rv4Ui9ZbT/aFlYgXIudMBUnh
14
+ U8SJrnXOTBRJ9+UlKIktHGUIdrut00uDi3NPo5DzudFOxu6/N2WMTZu/EN1ayx11
15
+ CoICz2gqq5l6c9bk2NI7WJmcwGi+8DZJbkS8ZJGGuQkRUu8wxkTO3XLVgdobm8sI
16
+ MObYYPP4NnGtf8Ntn8mTvwIcpbg/gDMin/W/z7sqfQKBgQDhwBgCR6g2bxx1aJ1S
17
+ q0KBh/XfsQg80Raohb22sant98PodZD4FubbuUpXx9kKm9B81bzlDRaNgOYEJ49g
18
+ ZJYnIXHRPh6UDrOcFzftL8b+I5P5oCZ4XWdHz7IH7H9KpY8dvhZqNkwoUN7RGGMD
19
+ 0HLjn0U7i2xa5TttOFgMwYLlWwKBgQDH4mnW4fj/mfQYsLOtcPXBugsYcEbbyd6C
20
+ KM2db7K+BRDZzCXHIKp1KPRo/2cxN+ykLHccuAJh9aOy8m84dLEI4tOEv6Q0Sa9c
21
+ rHp+vOtMUOYkXcn0rM0cU9Ns5S0jLbi7qXnjJ7aczZUtvFsxA63UR3yMCDK4rzj+
22
+ gQtInvspMwKBgQDfVe9qsgGUeLAq52hdFNki1KNGvhlsMV69MjLRv0piBrBmFYlq
23
+ Jx4VWmZWGXx7plLIbZwG7r/VFiR5D7oknt59r/SuEUqnJzRBxCasHIw9eG04lFv6
24
+ 0E0RGkUC2dHEw62muxvpz/XgHMGTExFCAMLotfUER5bXBdmY4Bkb5YXd6QKBgQCC
25
+ EjUbjyswcWellX8m82YJLd7AhXem2OOBwbmjpKck/jjr8ev3e6tQ2FjL5r+pCKJm
26
+ Z0UPnDJ4updPAHIdw9ncVXadYPQiznxeCyMfTCK2I8LPkXS1UqmasHXZ2/yWcs9O
27
+ 10co6ZPsz98uxu50o9c/V1GV8lPHWMb23tSP6ly4fwKBgDMe2/RSjX0FKT73q042
28
+ X5pbCZ2pi22m9nR0lzLI+5J8BXJ4FRn4ZKcWn1CBWJF/poR9yTlBRWiB+zLWrjiZ
29
+ SPEGFmA9k/ea+IiNrHoTTaMpNBkYAFCSxvjYy2ZrV9Umx/9ABLtkP0KUnfXLmmpL
30
+ T3SvNSq4WeFkfw8LIoda818x
31
+ -----END PRIVATE KEY-----
@@ -0,0 +1,122 @@
1
+ {
2
+ "common": {
3
+ "name": "leapmotor",
4
+ "version": "0.2.0",
5
+ "news": {
6
+ "0.2.0": {
7
+ "en": "Full release: status, consumption, pictures, composite HTML dashboard",
8
+ "de": "Vollversion: Status, Verbrauch, Bilder, HTML-Dashboard",
9
+ "ru": "Полный выпуск: статус, потребление, изображения, HTML-панель",
10
+ "pt": "Versão completa: status, consumo, imagens, painel HTML",
11
+ "nl": "Volledige release: status, verbruik, afbeeldingen, HTML-dashboard",
12
+ "fr": "Version complète: statut, consommation, images, tableau de bord HTML",
13
+ "it": "Versione completa: stato, consumo, immagini, dashboard HTML",
14
+ "es": "Versión completa: estado, consumo, imágenes, panel HTML",
15
+ "pl": "Pełna wersja: status, zużycie, obrazy, panel HTML",
16
+ "uk": "Повний випуск: статус, споживання, зображення, HTML-панель",
17
+ "zh-cn": "完整版本:状态、消耗、图片、HTML仪表板"
18
+ },
19
+ "0.1.0": {
20
+ "en": "Initial release",
21
+ "de": "Erste Veröffentlichung",
22
+ "ru": "Первый выпуск",
23
+ "pt": "Lançamento inicial",
24
+ "nl": "Eerste release",
25
+ "fr": "Première version",
26
+ "it": "Prima versione",
27
+ "es": "Versión inicial",
28
+ "pl": "Pierwsze wydanie",
29
+ "uk": "Перший випуск",
30
+ "zh-cn": "初始版本"
31
+ }
32
+ },
33
+ "titleLang": {
34
+ "en": "Leapmotor Vehicle Integration",
35
+ "de": "Leapmotor Fahrzeug Integration",
36
+ "ru": "Интеграция автомобиля Leapmotor",
37
+ "pt": "Integração de veículo Leapmotor",
38
+ "nl": "Leapmotor Voertuig Integratie",
39
+ "fr": "Intégration véhicule Leapmotor",
40
+ "it": "Integrazione veicolo Leapmotor",
41
+ "es": "Integración de vehículo Leapmotor",
42
+ "pl": "Integracja pojazdu Leapmotor",
43
+ "uk": "Інтеграція транспортного засобу Leapmotor",
44
+ "zh-cn": "零跑汽车集成"
45
+ },
46
+ "desc": {
47
+ "en": "Integrate Leapmotor electric vehicles into ioBroker. Supports status monitoring, remote control and real-time updates.",
48
+ "de": "Integration von Leapmotor Elektrofahrzeugen in ioBroker. Unterstützt Statusüberwachung, Fernsteuerung und Echtzeit-Updates.",
49
+ "ru": "Интеграция электромобилей Leapmotor в ioBroker.",
50
+ "pt": "Integre veículos elétricos Leapmotor no ioBroker.",
51
+ "nl": "Integreer Leapmotor elektrische voertuigen in ioBroker.",
52
+ "fr": "Intégrez les véhicules électriques Leapmotor dans ioBroker.",
53
+ "it": "Integra i veicoli elettrici Leapmotor in ioBroker.",
54
+ "es": "Integra vehículos eléctricos Leapmotor en ioBroker.",
55
+ "pl": "Integruj pojazdy elektryczne Leapmotor z ioBroker.",
56
+ "uk": "Інтегруйте електромобілі Leapmotor в ioBroker.",
57
+ "zh-cn": "将零跑电动汽车集成到ioBroker中。"
58
+ },
59
+ "authors": ["Henrik Schönhofen <henrik.schoenhofen@gmx.de>"],
60
+ "keywords": ["leapmotor", "electric vehicle", "EV", "T03"],
61
+ "licenseInformation": {
62
+ "license": "MIT",
63
+ "type": "free"
64
+ },
65
+ "platform": "Javascript/Node.js",
66
+ "icon": "leapmotor.png",
67
+ "extIcon": "https://raw.githubusercontent.com/backfisch88/ioBroker.leapmotor/main/admin/leapmotor.png",
68
+ "enabled": true,
69
+ "readme": "https://github.com/backfisch88/ioBroker.leapmotor/blob/main/README.md",
70
+ "loglevel": "info",
71
+ "mode": "daemon",
72
+ "type": "vehicle",
73
+ "tier": 3,
74
+ "compact": false,
75
+ "connectionType": "cloud",
76
+ "dataSource": "poll",
77
+ "adminUI": {
78
+ "config": "json"
79
+ },
80
+ "dependencies": [
81
+ {
82
+ "js-controller": ">=6.0.11"
83
+ }
84
+ ],
85
+ "globalDependencies": [
86
+ {
87
+ "admin": ">=7.6.17"
88
+ }
89
+ ],
90
+ "protectedNative": ["password", "operationPassword"],
91
+ "encryptedNative": ["password", "operationPassword"]
92
+ },
93
+ "native": {
94
+ "email": "",
95
+ "password": "",
96
+ "operationPassword": "",
97
+ "pollingInterval": 5,
98
+ "language": "en-GB"
99
+ },
100
+ "objects": [],
101
+ "instanceObjects": [
102
+ {
103
+ "_id": "info",
104
+ "type": "channel",
105
+ "common": { "name": "Information" },
106
+ "native": {}
107
+ },
108
+ {
109
+ "_id": "info.connection",
110
+ "type": "state",
111
+ "common": {
112
+ "name": "Connection status",
113
+ "type": "boolean",
114
+ "role": "indicator.connected",
115
+ "read": true,
116
+ "write": false,
117
+ "def": false
118
+ },
119
+ "native": {}
120
+ }
121
+ ]
122
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "iobroker.leapmotor",
3
+ "version": "0.2.0",
4
+ "description": "Leapmotor electric vehicle integration for ioBroker",
5
+ "author": {
6
+ "name": "Henrik Schönhofen",
7
+ "email": "henrik.schoenhofen@gmx.de"
8
+ },
9
+ "homepage": "https://github.com/backfisch88/ioBroker.leapmotor",
10
+ "license": "MIT",
11
+ "keywords": ["ioBroker", "leapmotor", "electric vehicle", "EV", "T03"],
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/backfisch88/ioBroker.leapmotor.git"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/backfisch88/ioBroker.leapmotor/issues"
18
+ },
19
+ "dependencies": {
20
+ "@iobroker/adapter-core": "^3.2.3",
21
+ "axios": "^1.17.0",
22
+ "node-forge": "^1.4.0",
23
+ "adm-zip": "^0.5.10"
24
+ },
25
+ "devDependencies": {
26
+ "@iobroker/testing": "^5.2.2",
27
+ "@types/node": "^20.0.0",
28
+ "@types/node-forge": "^1.3.14",
29
+ "typescript": "^5.0.0"
30
+ },
31
+ "main": "build/main.js",
32
+ "files": [
33
+ "admin/",
34
+ "build/",
35
+ "certs/",
36
+ "io-package.json",
37
+ "LICENSE"
38
+ ],
39
+ "scripts": {
40
+ "build": "tsc -p tsconfig.json",
41
+ "watch": "tsc -p tsconfig.json --watch",
42
+ "lint": "echo \"No linting configured\"",
43
+ "test": "echo \"No tests configured\""
44
+ },
45
+ "engines": {
46
+ "node": ">=20"
47
+ }
48
+ }