Skip to content

Commit 39c89a7

Browse files
committed
Add devices-zigbee-herdsman plugin for direct Zigbee device integration
Self-contained Zigbee device integration via zigbee-herdsman library. Communicates directly with USB and network-attached Zigbee coordinators (CC2652, ConBee, SLZB-06/07, etc.) without external dependencies. Backend plugin (NestJS): - ZigbeeHerdsmanAdapterService wrapping zigbee-herdsman Controller - ZigbeeHerdsmanService with IManagedPluginService lifecycle - fromZigbee message processing with property value writes - Mapping preview and device adoption services - Device platform handler with toZigbee converter dispatch - Device connectivity monitoring with offline detection - REST controller with Swagger-annotated endpoints - Config validator supporting USB serial and TCP paths - Entities with Zigbee-specific columns and TypeORM migration - Fix: shelly-ng resetReconnectInterval type error - Fix: missing @influxdata/influxdb-client dependency Admin UI (Vue.js): - Plugin registration with config and device type elements - Config form, device add/edit forms, store schemas, locales https://claude.ai/code/session_014bjB9Cn1WKASNLBeCuSbom
1 parent ce194d0 commit 39c89a7

63 files changed

Lines changed: 7185 additions & 468 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/admin/src/app.main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import { DevicesShellyNgPlugin } from './plugins/devices-shelly-ng';
7878
import { DevicesShellyV1Plugin } from './plugins/devices-shelly-v1';
7979
import { DevicesThirdPartyPlugin } from './plugins/devices-third-party';
8080
import { DevicesWledPlugin } from './plugins/devices-wled';
81+
import { DevicesZigbeeHerdsmanPlugin } from './plugins/devices-zigbee-herdsman';
8182
import { DevicesZigbee2mqttPlugin } from './plugins/devices-zigbee2mqtt';
8283
import { LoggerRotatingFilePlugin } from './plugins/logger-rotating-file';
8384
import { PagesCardsPlugin } from './plugins/pages-cards';
@@ -204,6 +205,7 @@ app.use(DevicesShellyNgPlugin, pluginOptions);
204205
app.use(DevicesShellyV1Plugin, pluginOptions);
205206
app.use(SimulatorPlugin, pluginOptions);
206207
app.use(DevicesWledPlugin, pluginOptions);
208+
app.use(DevicesZigbeeHerdsmanPlugin, pluginOptions);
207209
app.use(DevicesZigbee2mqttPlugin, pluginOptions);
208210
app.use(SpacesHomeControlPlugin, pluginOptions);
209211
app.use(SpacesSyntheticMasterPlugin, pluginOptions);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export { default as ZigbeeHerdsmanConfigForm } from './zigbee-herdsman-config-form.vue';
2+
export { default as ZigbeeHerdsmanDeviceAddForm } from './zigbee-herdsman-device-add-form.vue';
3+
export { default as ZigbeeHerdsmanDeviceEditForm } from './zigbee-herdsman-device-edit-form.vue';
4+
5+
export * from './types';
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './zigbee-herdsman-config-form.types';
2+
export * from './zigbee-herdsman-device-add-form.types';
3+
export * from './zigbee-herdsman-device-edit-form.types';
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { FormResultType, LayoutType } from '../../../modules/config';
2+
import type { IConfigPlugin } from '../../../modules/config/store/config-plugins.store.types';
3+
4+
export interface IZigbeeHerdsmanConfigFormProps {
5+
config: IConfigPlugin;
6+
remoteFormSubmit?: boolean;
7+
remoteFormResult?: FormResultType;
8+
remoteFormReset?: boolean;
9+
remoteFormChanged?: boolean;
10+
layout?: LayoutType;
11+
}
Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
<template>
2+
<el-form
3+
ref="formEl"
4+
:model="model"
5+
:rules="rules"
6+
label-position="top"
7+
status-icon
8+
>
9+
<el-alert
10+
type="info"
11+
:title="t('devicesZigbeeHerdsmanPlugin.headings.aboutPluginStatus')"
12+
:description="t('devicesZigbeeHerdsmanPlugin.texts.aboutPluginStatus')"
13+
:closable="false"
14+
/>
15+
16+
<el-form-item
17+
:label="t('devicesZigbeeHerdsmanPlugin.fields.config.enabled.title')"
18+
prop="enabled"
19+
label-position="left"
20+
class="mt-3"
21+
>
22+
<el-switch
23+
v-model="model.enabled"
24+
name="enabled"
25+
/>
26+
</el-form-item>
27+
28+
<hr />
29+
30+
<el-alert
31+
type="info"
32+
:title="t('devicesZigbeeHerdsmanPlugin.headings.aboutSerial')"
33+
:description="t('devicesZigbeeHerdsmanPlugin.texts.aboutSerial')"
34+
:closable="false"
35+
/>
36+
37+
<el-form-item
38+
:label="t('devicesZigbeeHerdsmanPlugin.fields.config.serial.path.title')"
39+
prop="serial.path"
40+
class="mt-3"
41+
>
42+
<el-input
43+
v-model="model.serial.path"
44+
:placeholder="t('devicesZigbeeHerdsmanPlugin.fields.config.serial.path.placeholder')"
45+
name="serialPath"
46+
/>
47+
</el-form-item>
48+
49+
<el-row :gutter="20">
50+
<el-col
51+
:xs="24"
52+
:sm="12"
53+
>
54+
<el-form-item
55+
:label="t('devicesZigbeeHerdsmanPlugin.fields.config.serial.baudRate.title')"
56+
prop="serial.baudRate"
57+
>
58+
<el-input-number
59+
v-model="model.serial.baudRate"
60+
:min="1"
61+
name="serialBaudRate"
62+
class="w-full!"
63+
/>
64+
</el-form-item>
65+
</el-col>
66+
<el-col
67+
:xs="24"
68+
:sm="12"
69+
>
70+
<el-form-item
71+
:label="t('devicesZigbeeHerdsmanPlugin.fields.config.serial.adapterType.title')"
72+
prop="serial.adapterType"
73+
>
74+
<el-input
75+
v-model="model.serial.adapterType"
76+
:placeholder="t('devicesZigbeeHerdsmanPlugin.fields.config.serial.adapterType.placeholder')"
77+
name="serialAdapterType"
78+
/>
79+
</el-form-item>
80+
</el-col>
81+
</el-row>
82+
83+
<hr />
84+
85+
<el-alert
86+
type="info"
87+
:title="t('devicesZigbeeHerdsmanPlugin.headings.aboutNetwork')"
88+
:description="t('devicesZigbeeHerdsmanPlugin.texts.aboutNetwork')"
89+
:closable="false"
90+
/>
91+
92+
<el-form-item
93+
:label="t('devicesZigbeeHerdsmanPlugin.fields.config.network.channel.title')"
94+
prop="network.channel"
95+
class="mt-3"
96+
>
97+
<el-input-number
98+
v-model="model.network.channel"
99+
:min="11"
100+
:max="26"
101+
name="networkChannel"
102+
class="w-full!"
103+
/>
104+
</el-form-item>
105+
106+
<hr />
107+
108+
<el-alert
109+
type="info"
110+
:title="t('devicesZigbeeHerdsmanPlugin.headings.aboutDiscovery')"
111+
:description="t('devicesZigbeeHerdsmanPlugin.texts.aboutDiscovery')"
112+
:closable="false"
113+
/>
114+
115+
<el-row
116+
:gutter="20"
117+
class="mt-3"
118+
>
119+
<el-col
120+
:xs="24"
121+
:sm="8"
122+
>
123+
<el-form-item
124+
:label="t('devicesZigbeeHerdsmanPlugin.fields.config.discovery.permitJoinTimeout.title')"
125+
prop="discovery.permitJoinTimeout"
126+
>
127+
<el-input-number
128+
v-model="model.discovery.permitJoinTimeout"
129+
:min="0"
130+
name="discoveryPermitJoinTimeout"
131+
class="w-full!"
132+
/>
133+
</el-form-item>
134+
</el-col>
135+
<el-col
136+
:xs="24"
137+
:sm="8"
138+
>
139+
<el-form-item
140+
:label="t('devicesZigbeeHerdsmanPlugin.fields.config.discovery.mainsDeviceTimeout.title')"
141+
prop="discovery.mainsDeviceTimeout"
142+
>
143+
<el-input-number
144+
v-model="model.discovery.mainsDeviceTimeout"
145+
:min="60"
146+
name="discoveryMainsDeviceTimeout"
147+
class="w-full!"
148+
/>
149+
</el-form-item>
150+
</el-col>
151+
<el-col
152+
:xs="24"
153+
:sm="8"
154+
>
155+
<el-form-item
156+
:label="t('devicesZigbeeHerdsmanPlugin.fields.config.discovery.batteryDeviceTimeout.title')"
157+
prop="discovery.batteryDeviceTimeout"
158+
>
159+
<el-input-number
160+
v-model="model.discovery.batteryDeviceTimeout"
161+
:min="60"
162+
name="discoveryBatteryDeviceTimeout"
163+
class="w-full!"
164+
/>
165+
</el-form-item>
166+
</el-col>
167+
</el-row>
168+
169+
<el-row :gutter="20">
170+
<el-col
171+
:xs="24"
172+
:sm="12"
173+
>
174+
<el-form-item
175+
:label="t('devicesZigbeeHerdsmanPlugin.fields.config.discovery.commandRetries.title')"
176+
prop="discovery.commandRetries"
177+
>
178+
<el-input-number
179+
v-model="model.discovery.commandRetries"
180+
:min="1"
181+
name="discoveryCommandRetries"
182+
class="w-full!"
183+
/>
184+
</el-form-item>
185+
</el-col>
186+
</el-row>
187+
188+
<el-form-item
189+
:label="t('devicesZigbeeHerdsmanPlugin.fields.config.discovery.syncOnStartup.title')"
190+
prop="discovery.syncOnStartup"
191+
label-position="left"
192+
>
193+
<el-switch
194+
v-model="model.discovery.syncOnStartup"
195+
name="discoverySyncOnStartup"
196+
/>
197+
</el-form-item>
198+
199+
<hr />
200+
201+
<el-alert
202+
type="info"
203+
:title="t('devicesZigbeeHerdsmanPlugin.headings.aboutDatabase')"
204+
:description="t('devicesZigbeeHerdsmanPlugin.texts.aboutDatabase')"
205+
:closable="false"
206+
/>
207+
208+
<el-form-item
209+
:label="t('devicesZigbeeHerdsmanPlugin.fields.config.databasePath.title')"
210+
prop="databasePath"
211+
class="mt-3"
212+
>
213+
<el-input
214+
v-model="model.databasePath"
215+
:placeholder="t('devicesZigbeeHerdsmanPlugin.fields.config.databasePath.placeholder')"
216+
name="databasePath"
217+
/>
218+
</el-form-item>
219+
</el-form>
220+
</template>
221+
222+
<script setup lang="ts">
223+
import { reactive, watch } from 'vue';
224+
import { useI18n } from 'vue-i18n';
225+
226+
import {
227+
ElAlert,
228+
ElCol,
229+
ElForm,
230+
ElFormItem,
231+
ElInput,
232+
ElInputNumber,
233+
ElRow,
234+
ElSwitch,
235+
type FormRules,
236+
} from 'element-plus';
237+
238+
import { FormResult, type FormResultType, Layout, useConfigPluginEditForm } from '../../../modules/config';
239+
import type { IZigbeeHerdsmanConfigEditForm } from '../schemas/config.types';
240+
241+
import type { IZigbeeHerdsmanConfigFormProps } from './zigbee-herdsman-config-form.types';
242+
243+
defineOptions({
244+
name: 'ZigbeeHerdsmanConfigForm',
245+
});
246+
247+
const props = withDefaults(defineProps<IZigbeeHerdsmanConfigFormProps>(), {
248+
remoteFormSubmit: false,
249+
remoteFormResult: FormResult.NONE,
250+
remoteFormReset: false,
251+
remoteFormChanged: false,
252+
layout: Layout.DEFAULT,
253+
});
254+
255+
const emit = defineEmits<{
256+
(e: 'update:remote-form-submit', remoteFormSubmit: boolean): void;
257+
(e: 'update:remote-form-result', remoteFormResult: FormResultType): void;
258+
(e: 'update:remote-form-reset', remoteFormReset: boolean): void;
259+
(e: 'update:remote-form-changed', formChanged: boolean): void;
260+
}>();
261+
262+
const { t } = useI18n();
263+
264+
const { formEl, model, formChanged, submit, formResult } = useConfigPluginEditForm<IZigbeeHerdsmanConfigEditForm>({
265+
config: props.config,
266+
messages: {
267+
success: t('devicesZigbeeHerdsmanPlugin.messages.config.edited'),
268+
error: t('devicesZigbeeHerdsmanPlugin.messages.config.notEdited'),
269+
},
270+
});
271+
272+
const rules = reactive<FormRules<IZigbeeHerdsmanConfigEditForm>>({
273+
'serial.path': [
274+
{ required: true, message: t('devicesZigbeeHerdsmanPlugin.fields.config.serial.path.validation.required'), trigger: 'blur' },
275+
],
276+
'serial.baudRate': [
277+
{
278+
type: 'integer',
279+
message: t('devicesZigbeeHerdsmanPlugin.fields.config.serial.baudRate.validation.number'),
280+
validator: (_rule, value) => value >= 1,
281+
trigger: 'change',
282+
},
283+
],
284+
'serial.adapterType': [
285+
{ required: true, message: t('devicesZigbeeHerdsmanPlugin.fields.config.serial.adapterType.validation.required'), trigger: 'blur' },
286+
],
287+
'network.channel': [
288+
{
289+
type: 'integer',
290+
message: t('devicesZigbeeHerdsmanPlugin.fields.config.network.channel.validation.range'),
291+
validator: (_rule, value) => value >= 11 && value <= 26,
292+
trigger: 'change',
293+
},
294+
],
295+
'databasePath': [
296+
{ required: true, message: t('devicesZigbeeHerdsmanPlugin.fields.config.databasePath.validation.required'), trigger: 'blur' },
297+
],
298+
});
299+
300+
watch(
301+
(): FormResultType => formResult.value,
302+
async (val: FormResultType): Promise<void> => {
303+
emit('update:remote-form-result', val);
304+
}
305+
);
306+
307+
watch(
308+
(): boolean => props.remoteFormSubmit,
309+
async (val: boolean): Promise<void> => {
310+
if (val) {
311+
emit('update:remote-form-submit', false);
312+
313+
submit().catch(() => {
314+
// The form is not valid
315+
});
316+
}
317+
}
318+
);
319+
320+
watch(
321+
(): boolean => props.remoteFormReset,
322+
(val: boolean): void => {
323+
if (val) {
324+
emit('update:remote-form-reset', false);
325+
326+
if (!formEl.value) return;
327+
328+
formEl.value.resetFields();
329+
}
330+
}
331+
);
332+
333+
watch(
334+
(): boolean => formChanged.value,
335+
(val: boolean): void => {
336+
emit('update:remote-form-changed', val);
337+
}
338+
);
339+
</script>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { IPluginElement } from '../../../common';
2+
import type { FormResultType } from '../../../modules/devices';
3+
import type { IZigbeeHerdsmanDevice } from '../store/devices.store.types';
4+
5+
export interface IZigbeeHerdsmanDeviceAddFormProps {
6+
id: IZigbeeHerdsmanDevice['id'];
7+
type: IPluginElement['type'];
8+
remoteFormSubmit?: boolean;
9+
remoteFormResult?: FormResultType;
10+
remoteFormReset?: boolean;
11+
remoteFormChanged?: boolean;
12+
}

0 commit comments

Comments
 (0)