Skip to content

Commit

Permalink
feat(add): amina S (#8191)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
somlioy authored Oct 31, 2024
1 parent 949b083 commit 24e4ab9
Show file tree
Hide file tree
Showing 2 changed files with 381 additions and 0 deletions.
379 changes: 379 additions & 0 deletions src/devices/amina.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,379 @@
import {Zcl} from 'zigbee-herdsman';

import * as constants from '../lib/constants';
import * as exposes from '../lib/exposes';
import {binary, deviceAddCustomCluster, electricityMeter, numeric, onOff} from '../lib/modernExtend';
import * as ota from '../lib/ota';
import * as reporting from '../lib/reporting';
import {DefinitionWithExtend, Fz, KeyValue, Tz} from '../lib/types';
import * as utils from '../lib/utils';

const e = exposes.presets;
const ea = exposes.access;

const manufacturerOptions = {manufacturerCode: 0x143b};

const aminaControlAttributes = {
cluster: 0xfee7,
alarms: 0x02,
ev_status: 0x03,
connect_status: 0x04,
single_phase: 0x05,
offline_current: 0x06,
offline_single_phase: 0x07,
time_to_offline: 0x08,
enable_offline: 0x09,
total_active_energy: 0x10,
last_session_energy: 0x11,
};

const aminaAlarms = [
'welded_relay',
'wrong_voltage_balance',
'rdc_dd_dc_leakage',
'rdc_dd_ac_leakage',
'high_temperature',
'overvoltage',
'undervoltage',
'overcurrent',
'car_communication_error',
'charger_processing_error',
'critical_overcurrent',
'critical_powerloss',
'unknown_alarm_bit_12',
'unknown_alarm_bit_13',
'unknown_alarm_bit_14',
'unknown_alarm_bit_15',
];

const aminaAlarmsEnum = e.enum('alarm', ea.STATE_GET, aminaAlarms);

const fzLocal = {
ev_status: {
cluster: 'aminaControlCluster',
type: ['attributeReport', 'readResponse'],
convert: (model, msg, publish, options, meta) => {
const result: KeyValue = {};

if (msg.data.evStatus !== undefined) {
let statusText = 'Not Connected';
const evStatus = msg.data['evStatus'];

result.ev_connected = (evStatus & (1 << 0)) !== 0;
result.charging = (evStatus & (1 << 2)) !== 0;
result.derated = (evStatus & (1 << 15)) !== 0;

if (result.ev_connected === true) statusText = 'EV Connected';
if ((evStatus & (1 << 1)) !== 0) statusText = 'Ready to charge';
if (result.charging === true) statusText = 'Charging';
if ((evStatus & (1 << 3)) !== 0) statusText = 'Charging Paused';

if (result.derated === true) statusText += ', Derated';

result.ev_status = statusText;

return result;
}
},
} satisfies Fz.Converter,

alarms: {
cluster: 'aminaControlCluster',
type: ['attributeReport', 'readResponse'],
convert: (model, msg, publish, options, meta) => {
const result: KeyValue = {};

if (msg.data.alarms !== undefined) {
result.alarm_active = false;

if (msg.data['alarms'] !== 0) {
result.alarms = aminaAlarmsEnum.values.filter((_, i) => (msg.data['alarms'] & (1 << i)) !== 0);
result.alarm_active = true;
}

return result;
}
},
} satisfies Fz.Converter,
};

const tzLocal = {
charge_limit: {
key: ['charge_limit'],
convertSet: async (entity, key, value, meta) => {
const payload = {level: value, transtime: 0};
await entity.command('genLevelCtrl', 'moveToLevel', payload, utils.getOptions(meta.mapped, entity));
},

convertGet: async (entity, key, meta) => {
await entity.read('genLevelCtrl', ['currentLevel'], manufacturerOptions);
},
} satisfies Tz.Converter,

ev_status: {
key: ['ev_status'],
convertGet: async (entity, key, meta) => {
await entity.read('aminaControlCluster', ['evStatus'], manufacturerOptions);
},
} satisfies Tz.Converter,

alarms: {
key: ['alarms'],
convertGet: async (entity, key, meta) => {
await entity.read('aminaControlCluster', ['alarms'], manufacturerOptions);
},
} satisfies Tz.Converter,
};

const definitions: DefinitionWithExtend[] = [
{
zigbeeModel: ['amina S'],
model: 'amina S',
vendor: 'Amina Distribution AS',
description: 'Amina S EV Charger',
ota: ota.zigbeeOTA,
fromZigbee: [fzLocal.ev_status, fzLocal.alarms],
toZigbee: [tzLocal.ev_status, tzLocal.alarms, tzLocal.charge_limit],
exposes: [
e.text('ev_status', ea.STATE_GET).withDescription('Current charging status'),
e.list('alarms', ea.STATE_GET, aminaAlarmsEnum).withDescription('List of active alarms'),
],
extend: [
deviceAddCustomCluster('aminaControlCluster', {
ID: aminaControlAttributes.cluster,
manufacturerCode: manufacturerOptions.manufacturerCode,
attributes: {
alarms: {ID: aminaControlAttributes.alarms, type: Zcl.DataType.BITMAP16},
evStatus: {ID: aminaControlAttributes.ev_status, type: Zcl.DataType.BITMAP16},
connectStatus: {ID: aminaControlAttributes.connect_status, type: Zcl.DataType.BITMAP16},
singlePhase: {ID: aminaControlAttributes.single_phase, type: Zcl.DataType.UINT8},
offlineCurrent: {ID: aminaControlAttributes.offline_current, type: Zcl.DataType.UINT8},
offlineSinglePhase: {ID: aminaControlAttributes.offline_single_phase, type: Zcl.DataType.UINT8},
timeToOffline: {ID: aminaControlAttributes.time_to_offline, type: Zcl.DataType.UINT16},
enableOffline: {ID: aminaControlAttributes.enable_offline, type: Zcl.DataType.UINT8},
totalActiveEnergy: {ID: aminaControlAttributes.total_active_energy, type: Zcl.DataType.UINT32},
lastSessionEnergy: {ID: aminaControlAttributes.last_session_energy, type: Zcl.DataType.UINT32},
},
commands: {},
commandsResponse: {},
}),

onOff({
powerOnBehavior: false,
}),

numeric({
name: 'charge_limit',
cluster: 'genLevelCtrl',
attribute: 'currentLevel',
description: 'Maximum allowed amperage draw',
reporting: {min: 0, max: 'MAX', change: 1},
unit: 'A',
valueMin: 6,
valueMax: 32,
valueStep: 1,
access: 'ALL',
}),

numeric({
name: 'total_active_power',
cluster: 'haElectricalMeasurement',
attribute: 'totalActivePower',
description: 'Instantaneous measured total active power',
reporting: {min: '10_SECONDS', max: 'MAX', change: 10},
unit: 'kW',
scale: 1000,
precision: 2,
access: 'STATE_GET',
}),

numeric({
name: 'total_active_energy',
cluster: 'aminaControlCluster',
attribute: 'totalActiveEnergy',
description: 'Sum of consumed energy',
//reporting: {min: '10_SECONDS', max: 'MAX', change: 5}, // Not Reportable atm, updated using onEvent
unit: 'kWh',
scale: 1000,
precision: 2,
access: 'STATE_GET',
}),

numeric({
name: 'last_session_energy',
cluster: 'aminaControlCluster',
attribute: 'lastSessionEnergy',
description: 'Sum of consumed energy last session',
//reporting: {min: '10_SECONDS', max: 'MAX', change: 5}, // Not Reportable atm, updated using onEvent
unit: 'kWh',
scale: 1000,
precision: 2,
access: 'STATE_GET',
}),

binary({
name: 'ev_connected',
cluster: 'aminaControlCluster',
attribute: 'evConnected',
description: 'An EV is connected to the charger',
valueOn: ['true', 1],
valueOff: ['false', 0],
access: 'STATE',
}),

binary({
name: 'charging',
cluster: 'aminaControlCluster',
attribute: 'charging',
description: 'Power is being delivered to the EV',
valueOn: ['true', 1],
valueOff: ['false', 0],
access: 'STATE',
}),

binary({
name: 'derated',
cluster: 'aminaControlCluster',
attribute: 'derated',
description: 'Charging derated due to high temperature',
valueOn: ['true', 1],
valueOff: ['false', 0],
access: 'STATE',
}),

binary({
name: 'alarm_active',
cluster: 'aminaControlCluster',
attribute: 'alarmActive',
description: 'An active alarm is present',
valueOn: ['true', 1],
valueOff: ['false', 0],
access: 'STATE',
}),

electricityMeter({
cluster: 'electrical',
acFrequency: true,
threePhase: true,
}),

binary({
name: 'single_phase',
cluster: 'aminaControlCluster',
attribute: 'singlePhase',
description: 'Enable single phase charging. A restart of charging is required for the change to take effect.',
valueOn: ['enable', 1],
valueOff: ['disable', 0],
entityCategory: 'config',
}),

binary({
name: 'enable_offline',
cluster: 'aminaControlCluster',
attribute: 'enableOffline',
description: 'Enable offline mode when connection to the network is lost',
valueOn: ['enable', 1],
valueOff: ['disable', 0],
entityCategory: 'config',
}),

numeric({
name: 'time_to_offline',
cluster: 'aminaControlCluster',
attribute: 'timeToOffline',
description: 'Time until charger will behave as offline after connection has been lost',
valueMin: 0,
valueMax: 60,
valueStep: 1,
unit: 's',
entityCategory: 'config',
}),

numeric({
name: 'offline_current',
cluster: 'aminaControlCluster',
attribute: 'offlineCurrent',
description: 'Maximum allowed amperage draw when device is offline',
valueMin: 6,
valueMax: 32,
valueStep: 1,
unit: 'A',
entityCategory: 'config',
}),

binary({
name: 'offline_single_phase',
cluster: 'aminaControlCluster',
attribute: 'offlineSinglePhase',
description: 'Use single phase charging when device is offline',
valueOn: ['enable', 1],
valueOff: ['disable', 0],
entityCategory: 'config',
}),
],

endpoint: (device) => {
return {default: 10};
},

configure: async (device, coordinatorEndpoint) => {
const endpoint = device.getEndpoint(10);

const binds = ['genBasic', 'genLevelCtrl', 'aminaControlCluster'];
await reporting.bind(endpoint, coordinatorEndpoint, binds);

await endpoint.configureReporting('aminaControlCluster', [
{
attribute: 'evStatus',
minimumReportInterval: 0,
maximumReportInterval: constants.repInterval.MAX,
reportableChange: 1,
},
]);

await endpoint.configureReporting('aminaControlCluster', [
{
attribute: 'alarms',
minimumReportInterval: 0,
maximumReportInterval: constants.repInterval.MAX,
reportableChange: 1,
},
]);

await endpoint.read('aminaControlCluster', [
'alarms',
'evStatus',
'connectStatus',
'singlePhase',
'offlineCurrent',
'offlineSinglePhase',
'timeToOffline',
'enableOffline',
'totalActiveEnergy',
'lastSessionEnergy',
]);
},

onEvent: async (type, data, device) => {
if (
type === 'message' &&
data.type === 'attributeReport' &&
data.cluster === 'haElectricalMeasurement' &&
data.data['totalActivePower']
) {
// Device does not support reporting of energy attributes, so we poll them manually when power is updated
await data.endpoint.read('aminaControlCluster', ['totalActiveEnergy']);
}

if (type === 'message' && data.type === 'attributeReport' && data.cluster === 'aminaControlCluster' && data.data['evStatus']) {
// Device does not support reporting of energy attributes, so we poll them manually when charging is stopped
if ((data.data['evStatus'] & (1 << 2)) === 0) {
await data.endpoint.read('aminaControlCluster', ['totalActiveEnergy', 'lastSessionEnergy']);
}
}
},
},
];

export default definitions;
module.exports = definitions;
2 changes: 2 additions & 0 deletions src/devices/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import akuvox from './akuvox';
import alchemy from './alchemy';
import aldi from './aldi';
import alecto from './alecto';
import amina from './amina';
import anchor from './anchor';
import atlantic from './atlantic';
import atsmart from './atsmart';
Expand Down Expand Up @@ -321,6 +322,7 @@ export default [
...alchemy,
...aldi,
...alecto,
...amina,
...anchor,
...atlantic,
...atsmart,
Expand Down

0 comments on commit 24e4ab9

Please sign in to comment.