Skip to content
Closed
Changes from 15 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4090914
Add clamped brightness and configuration options
xses79 Jan 3, 2026
10b40b3
Merge branch 'Koenkk:master' into master
xses79 Jan 3, 2026
55f3f29
Fix formatting in namron.ts
xses79 Jan 3, 2026
fdda243
Refactor return statement in namron.ts
xses79 Jan 3, 2026
de72b5a
Fix syntax error in namron.ts
xses79 Jan 3, 2026
fd8b139
Refactor dimmer functions for clarity and separation
xses79 Jan 3, 2026
983c412
Refactor dimmer conversion functions for clarity
xses79 Jan 3, 2026
15e0e12
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 3, 2026
fcb2bc4
Refactor dimmer converter for TypeScript safety
xses79 Jan 4, 2026
fdccfb4
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 4, 2026
78db30d
Update TypeScript-safe converters for dimmer
xses79 Jan 4, 2026
c2aa197
Update namron.ts
xses79 Jan 4, 2026
b7286f9
Update namron.ts
xses79 Jan 4, 2026
c22078d
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 4, 2026
95032f7
Merge branch 'master' into master
xses79 Jan 4, 2026
7cd21d5
Update namron.ts
xses79 Jan 5, 2026
80f3c4c
Merge branch 'Koenkk:master' into master
xses79 Jan 5, 2026
2aa75e5
Update namron.ts
xses79 Jan 5, 2026
6c66a4c
Update namron.ts
xses79 Jan 5, 2026
9a62bcf
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 5, 2026
31ce672
Merge branch 'master' into master
xses79 Jan 5, 2026
3f84af2
Merge branch 'master' into master
xses79 Jan 11, 2026
2929428
Merge branch 'master' into master
xses79 Feb 2, 2026
238893a
Merge branch 'master' into master
xses79 Feb 3, 2026
4162f82
Clean up Zigbee state/brightness handling
xses79 Feb 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 174 additions & 4 deletions src/devices/namron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,127 @@ const tzLocal = {
},
} satisfies Tz.Converter,
};
// -----------------------------------------------------------------------------
// Namron Simplify Dimmer (4512791) helpers + toZigbee converters
// Place this block BEFORE: export const definitions: DefinitionWithExtend[] = [
// -----------------------------------------------------------------------------

// Derive repo-correct types from Tz.Converter (avoids importing Meta/Endpoint/Group)
type TzConvertSet = NonNullable<Tz.Converter["convertSet"]>;
type TzEntity = Parameters<TzConvertSet>[0];
type TzMeta = Parameters<TzConvertSet>[3];

type TzConvertGet = NonNullable<Tz.Converter["convertGet"]>;
type TzGetEntity = Parameters<TzConvertGet>[0];
type TzGetMeta = Parameters<TzConvertGet>[2];

// Simplify Dimmer (4512791) — local toZigbee converters (repo-check-safe)
const sdClamp = (v: number, min: number, max: number) => Math.min(Math.max(v, min), max);
const sdSecToZclTime = (s: number) => Math.max(0, Math.round(Number(s || 0) * 10)); // ZCL = 1/10s

// Percent <-> level (1..254)
const sdPctToLevel = (pct: number) => sdClamp(Math.round((Number(pct) / 100) * 254), 1, 254);
const sdLevelToPct = (lvl: number) => sdClamp(Math.round((Number(lvl) / 254) * 100), 1, 100);

const tzLocalSimplifyDimmer4512791 = {
// Software clamp only -> stored in store, no device attribute to read => no convertGet
min_brightness: {
key: ["min_brightness"],
convertSet: (entity: TzEntity, key: string, value: unknown, meta: TzMeta) => {
const pct = Number(value);
if (!Number.isFinite(pct) || pct < 1 || pct > 50) throw new Error("min_brightness must be 1..50 (%)");
const lvl = sdClamp(sdPctToLevel(pct), 1, 127);

const maxLvl = store.getValue(meta.device, "max_brightness_level");
if (typeof maxLvl === "number" && lvl > maxLvl) {
throw new Error(`min_brightness (${pct}%) cannot exceed max_brightness (${sdLevelToPct(maxLvl)}%)`);
}

store.putValue(meta.device, "min_brightness_level", lvl);
return {state: {min_brightness: sdLevelToPct(lvl)}};
},
} satisfies Tz.Converter,

// Software clamp only -> stored in store, no device attribute to read => no convertGet
max_brightness: {
key: ["max_brightness"],
convertSet: (entity: TzEntity, key: string, value: unknown, meta: TzMeta) => {
const pct = Number(value);
if (!Number.isFinite(pct) || pct < 50 || pct > 100) throw new Error("max_brightness must be 50..100 (%)");
const lvl = sdClamp(sdPctToLevel(pct), 127, 254);

const minLvl = store.getValue(meta.device, "min_brightness_level");
if (typeof minLvl === "number" && lvl < minLvl) {
throw new Error(`max_brightness (${pct}%) cannot be below min_brightness (${sdLevelToPct(minLvl)}%)`);
}

store.putValue(meta.device, "max_brightness_level", lvl);
return {state: {max_brightness: sdLevelToPct(lvl)}};
},
} satisfies Tz.Converter,

// Software default transition only -> stored in store, we do NOT write unsupported attributes => no convertGet
dimming_speed: {
key: ["dimming_speed"],
convertSet: (entity: TzEntity, key: string, value: unknown, meta: TzMeta) => {
const s = Number(value);
if (!Number.isFinite(s) || s < 1 || s > 10) throw new Error("dimming_speed must be 1..10 seconds");
store.putValue(meta.device, "dimming_speed", s);
return {state: {dimming_speed: s}};
},
} satisfies Tz.Converter,

// Device-backed via genLevelCtrl.onLevel (startUpCurrentLevel unsupported on your device)
start_brightness: {
key: ["start_brightness"],
convertSet: async (entity: TzEntity, key: string, value: unknown, meta: TzMeta) => {
const v = Math.round(Number(value));
if (!Number.isFinite(v) || v < 1 || v > 254) throw new Error("start_brightness must be 1..254");
await entity.write("genLevelCtrl", {onLevel: v});
return {state: {start_brightness: v}};
},
// In this repo, convertGet must be Promise<void>: read attribute and let fromZigbee update state
convertGet: async (entity: TzGetEntity, key: string, meta: TzGetMeta): Promise<void> => {
await entity.read("genLevelCtrl", ["onLevel"]);
},
} satisfies Tz.Converter,

// Brightness set with clamp + required optionsMask/optionsOverride to avoid "optionsMask is missing"
brightness_clamped: {
key: ["brightness", "brightness_percent", "transition"],
convertSet: async (entity: TzEntity, key: string, value: unknown, meta: TzMeta) => {
// meta.message typing varies; keep it safe without any
const msg = (meta as unknown as {message?: Record<string, unknown>}).message ?? {};

let level = key === "brightness" ? Number(value) : sdPctToLevel(Number(value));
if (!Number.isFinite(level)) return;

const minLvl = store.getValue(meta.device, "min_brightness_level");
const maxLvl = store.getValue(meta.device, "max_brightness_level");
const minClamp = typeof minLvl === "number" ? minLvl : 1;
const maxClamp = typeof maxLvl === "number" ? maxLvl : 254;

level = Math.round(sdClamp(level, minClamp, maxClamp));

const storedSpeed = store.getValue(meta.device, "dimming_speed");
const transitionSec = msg["transition"] != null ? Number(msg["transition"]) : typeof storedSpeed === "number" ? storedSpeed : 0;

const transtime = sdSecToZclTime(transitionSec);

await entity.command(
"genLevelCtrl",
"moveToLevelWithOnOff",
{level, transtime, optionsMask: 0, optionsOverride: 0},
{disableDefaultResponse: true},
);

return {state: {state: "ON", brightness: level}};
},
} satisfies Tz.Converter,
};
// -----------------------------------------------------------------------------
// End Simplify Dimmer (4512791)
// -----------------------------------------------------------------------------

export const definitions: DefinitionWithExtend[] = [
{
Expand Down Expand Up @@ -1780,19 +1901,68 @@ export const definitions: DefinitionWithExtend[] = [
model: "4512791",
vendor: "Namron",
description: "Namron Simplify Zigbee dimmer (1/2-polet / Zigbee / BT)",

// Modern extend kun for målinger (ingen effect)
extend: [
m.light({}),
m.electricityMeter({
power: {multiplier: 1, divisor: 10},
voltage: {multiplier: 1, divisor: 10},
current: {multiplier: 1, divisor: 100},
energy: {multiplier: 1, divisor: 100},
}),
],

// Sørger for at state/brightness kommer inn (uten å dra inn modern light/effect)
fromZigbee: [fz.on_off, fz.brightness],

// On/off + clamped brightness + config setters
toZigbee: [
tz.on_off,
tzLocalSimplifyDimmer4512791.brightness_clamped,
tzLocalSimplifyDimmer4512791.min_brightness,
tzLocalSimplifyDimmer4512791.max_brightness,
tzLocalSimplifyDimmer4512791.dimming_speed,
tzLocalSimplifyDimmer4512791.start_brightness,
],

exposes: [
exposes.numeric("min_brightness", ea.ALL).withValueMin(1).withValueMax(127).withDescription("Minimum brightness (≈1–50%)"),
exposes.numeric("max_brightness", ea.ALL).withValueMin(127).withValueMax(254).withDescription("Maximum brightness (≈50–100%)"),
exposes.numeric("start_brightness", ea.ALL).withValueMin(1).withValueMax(254).withDescription("Default brightness at power-on/startup"),
// Gir state + brightness uten effect
e.light_brightness(),

exposes
.numeric("min_brightness", ea.ALL)
.withValueMin(1)
.withValueMax(50)
.withDescription("Minimum brightness in % (1–50). Used to clamp brightness.")
.withCategory("config"),

exposes
.numeric("max_brightness", ea.ALL)
.withValueMin(50)
.withValueMax(100)
.withDescription("Maximum brightness in % (50–100). Used to clamp brightness.")
.withCategory("config"),

exposes
.numeric("dimming_speed", ea.ALL)
.withValueMin(1)
.withValueMax(10)
.withDescription("Default dimming time in seconds (1–10). Used as default transition for brightness commands.")
.withCategory("config"),

exposes
.numeric("start_brightness", ea.ALL)
.withValueMin(1)
.withValueMax(254)
.withDescription("On-level (1–254). startUpCurrentLevel unsupported on this device.")
.withCategory("config"),
],

configure: (device) => {
// Defaults så de ikke blir null
store.putValue(device, "min_brightness_level", sdPctToLevel(20));
store.putValue(device, "max_brightness_level", sdPctToLevel(100));
store.putValue(device, "dimming_speed", 1);
},
},
];