Skip to content

Commit 24e4ab9

Browse files
authored
feat(add): amina S (#8191)
* Added Amina S device * Cleanup and converted most exposes to extend * Removed unnecessary binds * Binary values to lowercase * Lint-Fix, removed simulation variables * Added polling of energy attributes in onEvent * Fixed typo * Implemented e.list() on alarms * Removed 'no_alarm' from alarms-enum * Create alarms array using .filter instead
1 parent 949b083 commit 24e4ab9

File tree

2 files changed

+381
-0
lines changed

2 files changed

+381
-0
lines changed

src/devices/amina.ts

+379
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
1+
import {Zcl} from 'zigbee-herdsman';
2+
3+
import * as constants from '../lib/constants';
4+
import * as exposes from '../lib/exposes';
5+
import {binary, deviceAddCustomCluster, electricityMeter, numeric, onOff} from '../lib/modernExtend';
6+
import * as ota from '../lib/ota';
7+
import * as reporting from '../lib/reporting';
8+
import {DefinitionWithExtend, Fz, KeyValue, Tz} from '../lib/types';
9+
import * as utils from '../lib/utils';
10+
11+
const e = exposes.presets;
12+
const ea = exposes.access;
13+
14+
const manufacturerOptions = {manufacturerCode: 0x143b};
15+
16+
const aminaControlAttributes = {
17+
cluster: 0xfee7,
18+
alarms: 0x02,
19+
ev_status: 0x03,
20+
connect_status: 0x04,
21+
single_phase: 0x05,
22+
offline_current: 0x06,
23+
offline_single_phase: 0x07,
24+
time_to_offline: 0x08,
25+
enable_offline: 0x09,
26+
total_active_energy: 0x10,
27+
last_session_energy: 0x11,
28+
};
29+
30+
const aminaAlarms = [
31+
'welded_relay',
32+
'wrong_voltage_balance',
33+
'rdc_dd_dc_leakage',
34+
'rdc_dd_ac_leakage',
35+
'high_temperature',
36+
'overvoltage',
37+
'undervoltage',
38+
'overcurrent',
39+
'car_communication_error',
40+
'charger_processing_error',
41+
'critical_overcurrent',
42+
'critical_powerloss',
43+
'unknown_alarm_bit_12',
44+
'unknown_alarm_bit_13',
45+
'unknown_alarm_bit_14',
46+
'unknown_alarm_bit_15',
47+
];
48+
49+
const aminaAlarmsEnum = e.enum('alarm', ea.STATE_GET, aminaAlarms);
50+
51+
const fzLocal = {
52+
ev_status: {
53+
cluster: 'aminaControlCluster',
54+
type: ['attributeReport', 'readResponse'],
55+
convert: (model, msg, publish, options, meta) => {
56+
const result: KeyValue = {};
57+
58+
if (msg.data.evStatus !== undefined) {
59+
let statusText = 'Not Connected';
60+
const evStatus = msg.data['evStatus'];
61+
62+
result.ev_connected = (evStatus & (1 << 0)) !== 0;
63+
result.charging = (evStatus & (1 << 2)) !== 0;
64+
result.derated = (evStatus & (1 << 15)) !== 0;
65+
66+
if (result.ev_connected === true) statusText = 'EV Connected';
67+
if ((evStatus & (1 << 1)) !== 0) statusText = 'Ready to charge';
68+
if (result.charging === true) statusText = 'Charging';
69+
if ((evStatus & (1 << 3)) !== 0) statusText = 'Charging Paused';
70+
71+
if (result.derated === true) statusText += ', Derated';
72+
73+
result.ev_status = statusText;
74+
75+
return result;
76+
}
77+
},
78+
} satisfies Fz.Converter,
79+
80+
alarms: {
81+
cluster: 'aminaControlCluster',
82+
type: ['attributeReport', 'readResponse'],
83+
convert: (model, msg, publish, options, meta) => {
84+
const result: KeyValue = {};
85+
86+
if (msg.data.alarms !== undefined) {
87+
result.alarm_active = false;
88+
89+
if (msg.data['alarms'] !== 0) {
90+
result.alarms = aminaAlarmsEnum.values.filter((_, i) => (msg.data['alarms'] & (1 << i)) !== 0);
91+
result.alarm_active = true;
92+
}
93+
94+
return result;
95+
}
96+
},
97+
} satisfies Fz.Converter,
98+
};
99+
100+
const tzLocal = {
101+
charge_limit: {
102+
key: ['charge_limit'],
103+
convertSet: async (entity, key, value, meta) => {
104+
const payload = {level: value, transtime: 0};
105+
await entity.command('genLevelCtrl', 'moveToLevel', payload, utils.getOptions(meta.mapped, entity));
106+
},
107+
108+
convertGet: async (entity, key, meta) => {
109+
await entity.read('genLevelCtrl', ['currentLevel'], manufacturerOptions);
110+
},
111+
} satisfies Tz.Converter,
112+
113+
ev_status: {
114+
key: ['ev_status'],
115+
convertGet: async (entity, key, meta) => {
116+
await entity.read('aminaControlCluster', ['evStatus'], manufacturerOptions);
117+
},
118+
} satisfies Tz.Converter,
119+
120+
alarms: {
121+
key: ['alarms'],
122+
convertGet: async (entity, key, meta) => {
123+
await entity.read('aminaControlCluster', ['alarms'], manufacturerOptions);
124+
},
125+
} satisfies Tz.Converter,
126+
};
127+
128+
const definitions: DefinitionWithExtend[] = [
129+
{
130+
zigbeeModel: ['amina S'],
131+
model: 'amina S',
132+
vendor: 'Amina Distribution AS',
133+
description: 'Amina S EV Charger',
134+
ota: ota.zigbeeOTA,
135+
fromZigbee: [fzLocal.ev_status, fzLocal.alarms],
136+
toZigbee: [tzLocal.ev_status, tzLocal.alarms, tzLocal.charge_limit],
137+
exposes: [
138+
e.text('ev_status', ea.STATE_GET).withDescription('Current charging status'),
139+
e.list('alarms', ea.STATE_GET, aminaAlarmsEnum).withDescription('List of active alarms'),
140+
],
141+
extend: [
142+
deviceAddCustomCluster('aminaControlCluster', {
143+
ID: aminaControlAttributes.cluster,
144+
manufacturerCode: manufacturerOptions.manufacturerCode,
145+
attributes: {
146+
alarms: {ID: aminaControlAttributes.alarms, type: Zcl.DataType.BITMAP16},
147+
evStatus: {ID: aminaControlAttributes.ev_status, type: Zcl.DataType.BITMAP16},
148+
connectStatus: {ID: aminaControlAttributes.connect_status, type: Zcl.DataType.BITMAP16},
149+
singlePhase: {ID: aminaControlAttributes.single_phase, type: Zcl.DataType.UINT8},
150+
offlineCurrent: {ID: aminaControlAttributes.offline_current, type: Zcl.DataType.UINT8},
151+
offlineSinglePhase: {ID: aminaControlAttributes.offline_single_phase, type: Zcl.DataType.UINT8},
152+
timeToOffline: {ID: aminaControlAttributes.time_to_offline, type: Zcl.DataType.UINT16},
153+
enableOffline: {ID: aminaControlAttributes.enable_offline, type: Zcl.DataType.UINT8},
154+
totalActiveEnergy: {ID: aminaControlAttributes.total_active_energy, type: Zcl.DataType.UINT32},
155+
lastSessionEnergy: {ID: aminaControlAttributes.last_session_energy, type: Zcl.DataType.UINT32},
156+
},
157+
commands: {},
158+
commandsResponse: {},
159+
}),
160+
161+
onOff({
162+
powerOnBehavior: false,
163+
}),
164+
165+
numeric({
166+
name: 'charge_limit',
167+
cluster: 'genLevelCtrl',
168+
attribute: 'currentLevel',
169+
description: 'Maximum allowed amperage draw',
170+
reporting: {min: 0, max: 'MAX', change: 1},
171+
unit: 'A',
172+
valueMin: 6,
173+
valueMax: 32,
174+
valueStep: 1,
175+
access: 'ALL',
176+
}),
177+
178+
numeric({
179+
name: 'total_active_power',
180+
cluster: 'haElectricalMeasurement',
181+
attribute: 'totalActivePower',
182+
description: 'Instantaneous measured total active power',
183+
reporting: {min: '10_SECONDS', max: 'MAX', change: 10},
184+
unit: 'kW',
185+
scale: 1000,
186+
precision: 2,
187+
access: 'STATE_GET',
188+
}),
189+
190+
numeric({
191+
name: 'total_active_energy',
192+
cluster: 'aminaControlCluster',
193+
attribute: 'totalActiveEnergy',
194+
description: 'Sum of consumed energy',
195+
//reporting: {min: '10_SECONDS', max: 'MAX', change: 5}, // Not Reportable atm, updated using onEvent
196+
unit: 'kWh',
197+
scale: 1000,
198+
precision: 2,
199+
access: 'STATE_GET',
200+
}),
201+
202+
numeric({
203+
name: 'last_session_energy',
204+
cluster: 'aminaControlCluster',
205+
attribute: 'lastSessionEnergy',
206+
description: 'Sum of consumed energy last session',
207+
//reporting: {min: '10_SECONDS', max: 'MAX', change: 5}, // Not Reportable atm, updated using onEvent
208+
unit: 'kWh',
209+
scale: 1000,
210+
precision: 2,
211+
access: 'STATE_GET',
212+
}),
213+
214+
binary({
215+
name: 'ev_connected',
216+
cluster: 'aminaControlCluster',
217+
attribute: 'evConnected',
218+
description: 'An EV is connected to the charger',
219+
valueOn: ['true', 1],
220+
valueOff: ['false', 0],
221+
access: 'STATE',
222+
}),
223+
224+
binary({
225+
name: 'charging',
226+
cluster: 'aminaControlCluster',
227+
attribute: 'charging',
228+
description: 'Power is being delivered to the EV',
229+
valueOn: ['true', 1],
230+
valueOff: ['false', 0],
231+
access: 'STATE',
232+
}),
233+
234+
binary({
235+
name: 'derated',
236+
cluster: 'aminaControlCluster',
237+
attribute: 'derated',
238+
description: 'Charging derated due to high temperature',
239+
valueOn: ['true', 1],
240+
valueOff: ['false', 0],
241+
access: 'STATE',
242+
}),
243+
244+
binary({
245+
name: 'alarm_active',
246+
cluster: 'aminaControlCluster',
247+
attribute: 'alarmActive',
248+
description: 'An active alarm is present',
249+
valueOn: ['true', 1],
250+
valueOff: ['false', 0],
251+
access: 'STATE',
252+
}),
253+
254+
electricityMeter({
255+
cluster: 'electrical',
256+
acFrequency: true,
257+
threePhase: true,
258+
}),
259+
260+
binary({
261+
name: 'single_phase',
262+
cluster: 'aminaControlCluster',
263+
attribute: 'singlePhase',
264+
description: 'Enable single phase charging. A restart of charging is required for the change to take effect.',
265+
valueOn: ['enable', 1],
266+
valueOff: ['disable', 0],
267+
entityCategory: 'config',
268+
}),
269+
270+
binary({
271+
name: 'enable_offline',
272+
cluster: 'aminaControlCluster',
273+
attribute: 'enableOffline',
274+
description: 'Enable offline mode when connection to the network is lost',
275+
valueOn: ['enable', 1],
276+
valueOff: ['disable', 0],
277+
entityCategory: 'config',
278+
}),
279+
280+
numeric({
281+
name: 'time_to_offline',
282+
cluster: 'aminaControlCluster',
283+
attribute: 'timeToOffline',
284+
description: 'Time until charger will behave as offline after connection has been lost',
285+
valueMin: 0,
286+
valueMax: 60,
287+
valueStep: 1,
288+
unit: 's',
289+
entityCategory: 'config',
290+
}),
291+
292+
numeric({
293+
name: 'offline_current',
294+
cluster: 'aminaControlCluster',
295+
attribute: 'offlineCurrent',
296+
description: 'Maximum allowed amperage draw when device is offline',
297+
valueMin: 6,
298+
valueMax: 32,
299+
valueStep: 1,
300+
unit: 'A',
301+
entityCategory: 'config',
302+
}),
303+
304+
binary({
305+
name: 'offline_single_phase',
306+
cluster: 'aminaControlCluster',
307+
attribute: 'offlineSinglePhase',
308+
description: 'Use single phase charging when device is offline',
309+
valueOn: ['enable', 1],
310+
valueOff: ['disable', 0],
311+
entityCategory: 'config',
312+
}),
313+
],
314+
315+
endpoint: (device) => {
316+
return {default: 10};
317+
},
318+
319+
configure: async (device, coordinatorEndpoint) => {
320+
const endpoint = device.getEndpoint(10);
321+
322+
const binds = ['genBasic', 'genLevelCtrl', 'aminaControlCluster'];
323+
await reporting.bind(endpoint, coordinatorEndpoint, binds);
324+
325+
await endpoint.configureReporting('aminaControlCluster', [
326+
{
327+
attribute: 'evStatus',
328+
minimumReportInterval: 0,
329+
maximumReportInterval: constants.repInterval.MAX,
330+
reportableChange: 1,
331+
},
332+
]);
333+
334+
await endpoint.configureReporting('aminaControlCluster', [
335+
{
336+
attribute: 'alarms',
337+
minimumReportInterval: 0,
338+
maximumReportInterval: constants.repInterval.MAX,
339+
reportableChange: 1,
340+
},
341+
]);
342+
343+
await endpoint.read('aminaControlCluster', [
344+
'alarms',
345+
'evStatus',
346+
'connectStatus',
347+
'singlePhase',
348+
'offlineCurrent',
349+
'offlineSinglePhase',
350+
'timeToOffline',
351+
'enableOffline',
352+
'totalActiveEnergy',
353+
'lastSessionEnergy',
354+
]);
355+
},
356+
357+
onEvent: async (type, data, device) => {
358+
if (
359+
type === 'message' &&
360+
data.type === 'attributeReport' &&
361+
data.cluster === 'haElectricalMeasurement' &&
362+
data.data['totalActivePower']
363+
) {
364+
// Device does not support reporting of energy attributes, so we poll them manually when power is updated
365+
await data.endpoint.read('aminaControlCluster', ['totalActiveEnergy']);
366+
}
367+
368+
if (type === 'message' && data.type === 'attributeReport' && data.cluster === 'aminaControlCluster' && data.data['evStatus']) {
369+
// Device does not support reporting of energy attributes, so we poll them manually when charging is stopped
370+
if ((data.data['evStatus'] & (1 << 2)) === 0) {
371+
await data.endpoint.read('aminaControlCluster', ['totalActiveEnergy', 'lastSessionEnergy']);
372+
}
373+
}
374+
},
375+
},
376+
];
377+
378+
export default definitions;
379+
module.exports = definitions;

src/devices/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import akuvox from './akuvox';
1010
import alchemy from './alchemy';
1111
import aldi from './aldi';
1212
import alecto from './alecto';
13+
import amina from './amina';
1314
import anchor from './anchor';
1415
import atlantic from './atlantic';
1516
import atsmart from './atsmart';
@@ -321,6 +322,7 @@ export default [
321322
...alchemy,
322323
...aldi,
323324
...alecto,
325+
...amina,
324326
...anchor,
325327
...atlantic,
326328
...atsmart,

0 commit comments

Comments
 (0)