@feezal/feezal-element 3.0.1 → 3.0.3
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/feezal-element.js +168 -0
- package/feezal-topic-input.js +1 -1
- package/package.json +1 -1
package/feezal-element.js
CHANGED
|
@@ -60,6 +60,28 @@ export class FeezalElement extends LitElement {
|
|
|
60
60
|
// bound to MQTT topics). Never reflected — the attribute is the
|
|
61
61
|
// source of truth; effects only apply in the viewer.
|
|
62
62
|
conditions: {type: String, attribute: 'conditions'},
|
|
63
|
+
// ── N31: availability — universal base-class machinery ───────────
|
|
64
|
+
// subscribe-availability accepts a single topic string (back-compat)
|
|
65
|
+
// OR a JSON array of topics / {topic, property} entries (the modern
|
|
66
|
+
// HA discovery form: bridge state + device availability). The base
|
|
67
|
+
// subscribes, tracks per-topic status and combines it per
|
|
68
|
+
// availability-mode into the reactive `_available` flag; rendering
|
|
69
|
+
// (badges, dimming) stays element business. Elements that want the
|
|
70
|
+
// fields in their inspector declare the attributes in their own
|
|
71
|
+
// descriptor — the machinery works regardless.
|
|
72
|
+
// Deliberately NOT reflected: Lit reflects constructor defaults on
|
|
73
|
+
// first update, which would stamp subscribe-availability="" /
|
|
74
|
+
// payload-available="online" / … onto EVERY element and serialize
|
|
75
|
+
// that junk into every saved dashboard (and it broke the U32
|
|
76
|
+
// create-component attribute table). The inspector and discovery
|
|
77
|
+
// write the attributes directly; attribute → property sync is all
|
|
78
|
+
// the machinery needs.
|
|
79
|
+
subscribeAvailability: {type: String, attribute: 'subscribe-availability'},
|
|
80
|
+
availabilityMode: {type: String, attribute: 'availability-mode'},
|
|
81
|
+
payloadAvailable: {type: String, attribute: 'payload-available'},
|
|
82
|
+
payloadUnavailable: {type: String, attribute: 'payload-unavailable'},
|
|
83
|
+
msgPropAvailability: {type: String, attribute: 'message-property-availability'},
|
|
84
|
+
_available: {state: true},
|
|
63
85
|
};
|
|
64
86
|
|
|
65
87
|
constructor() {
|
|
@@ -70,6 +92,15 @@ export class FeezalElement extends LitElement {
|
|
|
70
92
|
this.dynamicSubscriptions = false;
|
|
71
93
|
this.visible = false;
|
|
72
94
|
this._conditions = new FeezalConditions(this);
|
|
95
|
+
// N31 availability defaults (payload values = HA defaults)
|
|
96
|
+
this.subscribeAvailability = '';
|
|
97
|
+
this.availabilityMode = 'all';
|
|
98
|
+
this.payloadAvailable = 'online';
|
|
99
|
+
this.payloadUnavailable = 'offline';
|
|
100
|
+
this.msgPropAvailability = '';
|
|
101
|
+
this._available = true;
|
|
102
|
+
this._availabilitySubs = [];
|
|
103
|
+
this._availabilityStatus = {};
|
|
73
104
|
}
|
|
74
105
|
|
|
75
106
|
connectedCallback() {
|
|
@@ -79,11 +110,17 @@ export class FeezalElement extends LitElement {
|
|
|
79
110
|
this._subscribe();
|
|
80
111
|
this._conditions.connect();
|
|
81
112
|
}
|
|
113
|
+
// N31: availability is independent of the primary-subscription
|
|
114
|
+
// machinery (elements overriding _subscribe with an empty body still
|
|
115
|
+
// get it) and not editor-gated — it only feeds internal state, never
|
|
116
|
+
// writes serializable attributes in the editor.
|
|
117
|
+
this._subscribeAvailability();
|
|
82
118
|
}
|
|
83
119
|
|
|
84
120
|
disconnectedCallback() {
|
|
85
121
|
super.disconnectedCallback();
|
|
86
122
|
this._unsubscribe();
|
|
123
|
+
this._unsubscribeAvailability();
|
|
87
124
|
this._conditions.disconnect();
|
|
88
125
|
}
|
|
89
126
|
|
|
@@ -104,6 +141,98 @@ export class FeezalElement extends LitElement {
|
|
|
104
141
|
if (changed.has('conditions') && (this.visible || !this.dynamicSubscriptions)) {
|
|
105
142
|
this._conditions.connect();
|
|
106
143
|
}
|
|
144
|
+
|
|
145
|
+
// N31: availability wiring changed at runtime (inspector edits on the
|
|
146
|
+
// live canvas) → rewire. Signature-guarded so the first update after
|
|
147
|
+
// connect is a no-op.
|
|
148
|
+
if (this.isConnected && this.__availSig !== undefined &&
|
|
149
|
+
this._availabilitySignature() !== this.__availSig) {
|
|
150
|
+
this._unsubscribeAvailability();
|
|
151
|
+
this._subscribeAvailability();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── N31: availability ────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
_availabilitySignature() {
|
|
158
|
+
return [this.subscribeAvailability, this.availabilityMode,
|
|
159
|
+
this.payloadAvailable, this.payloadUnavailable, this.msgPropAvailability].join('|');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Parse subscribe-availability: '' → [], plain topic string → one entry,
|
|
163
|
+
* JSON array of strings / {topic, property} objects → many entries. */
|
|
164
|
+
_availabilityEntries() {
|
|
165
|
+
const raw = (this.subscribeAvailability || '').trim();
|
|
166
|
+
if (!raw) return [];
|
|
167
|
+
if (raw.startsWith('[')) {
|
|
168
|
+
try {
|
|
169
|
+
const arr = JSON.parse(raw);
|
|
170
|
+
return (Array.isArray(arr) ? arr : [])
|
|
171
|
+
.map(entry => (typeof entry === 'string' ? {topic: entry} : entry))
|
|
172
|
+
.filter(entry => entry && typeof entry.topic === 'string' && entry.topic);
|
|
173
|
+
} catch {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return [{topic: raw}];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
_subscribeAvailability() {
|
|
181
|
+
this.__availSig = this._availabilitySignature();
|
|
182
|
+
this._availabilityStatus = {};
|
|
183
|
+
const entries = this._availabilityEntries();
|
|
184
|
+
if (entries.length === 0) {
|
|
185
|
+
this._setAvailable(true);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
entries.forEach((entry, index) => {
|
|
189
|
+
const key = index + ':' + entry.topic;
|
|
190
|
+
this._availabilityStatus[key] = null; // unknown until the first message
|
|
191
|
+
this._availabilitySubs.push(feezal.connection.sub(entry.topic, msg => {
|
|
192
|
+
let v = this.getProperty(msg, entry.property || this.msgPropAvailability || this.messageProperty);
|
|
193
|
+
// Unwrap JSON {"state": "online"} payloads (zigbee2mqtt availability)
|
|
194
|
+
if (typeof v === 'string') {
|
|
195
|
+
try {
|
|
196
|
+
const parsed = JSON.parse(v);
|
|
197
|
+
if (parsed && typeof parsed === 'object' && 'state' in parsed) v = parsed.state;
|
|
198
|
+
} catch { /* not JSON — use the raw string */ }
|
|
199
|
+
} else if (v && typeof v === 'object' && 'state' in v) {
|
|
200
|
+
v = v.state;
|
|
201
|
+
}
|
|
202
|
+
const s = String(v).toLowerCase();
|
|
203
|
+
this._availabilityStatus[key] = String(v) === this.payloadAvailable ||
|
|
204
|
+
(s !== String(this.payloadUnavailable).toLowerCase() &&
|
|
205
|
+
s !== 'offline' && s !== 'false' && s !== '0' && s !== 'unavailable');
|
|
206
|
+
this._recomputeAvailability();
|
|
207
|
+
}));
|
|
208
|
+
});
|
|
209
|
+
this._recomputeAvailability();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
_unsubscribeAvailability() {
|
|
213
|
+
this._availabilitySubs.forEach(sub => feezal.connection.unsubscribe(sub));
|
|
214
|
+
this._availabilitySubs = [];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Combine per-topic status per availability-mode into `_available`.
|
|
218
|
+
* Topics without a message yet count as available (matches the previous
|
|
219
|
+
* per-element behaviour — an element never starts out badged). */
|
|
220
|
+
_recomputeAvailability() {
|
|
221
|
+
const statuses = Object.values(this._availabilityStatus);
|
|
222
|
+
const known = statuses.filter(v => v !== null);
|
|
223
|
+
this._setAvailable(this.availabilityMode === 'any'
|
|
224
|
+
? (known.length === 0 || known.some(Boolean))
|
|
225
|
+
: statuses.every(v => v !== false));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
_setAvailable(available) {
|
|
229
|
+
this._available = available;
|
|
230
|
+
// Styling hook for themes/zero-effort elements — viewer only: a
|
|
231
|
+
// reflected attribute in the editor would be serialized into the
|
|
232
|
+
// saved views.html.
|
|
233
|
+
if (!feezal.isEditor) {
|
|
234
|
+
this.toggleAttribute('unavailable', !available);
|
|
235
|
+
}
|
|
107
236
|
}
|
|
108
237
|
|
|
109
238
|
// ── Subscription helpers ─────────────────────────────────────────────────
|
|
@@ -262,5 +391,44 @@ export class FeezalElement extends LitElement {
|
|
|
262
391
|
}
|
|
263
392
|
}
|
|
264
393
|
|
|
394
|
+
// ── N31: shared availability badge (optional consistency helper) ─────────────
|
|
395
|
+
// Elements MAY compose these into their render/styles instead of hand-rolling
|
|
396
|
+
// a badge — the base class itself renders nothing. Themable via the
|
|
397
|
+
// --feezal-unavailable-* custom properties.
|
|
398
|
+
export const feezalAvailabilityStyles = css`
|
|
399
|
+
.feezal-unavail-badge {
|
|
400
|
+
position: absolute;
|
|
401
|
+
top: var(--feezal-unavailable-top, 4px);
|
|
402
|
+
right: var(--feezal-unavailable-right, 4px);
|
|
403
|
+
font-size: var(--feezal-unavailable-size, 14px);
|
|
404
|
+
line-height: 1;
|
|
405
|
+
color: var(--feezal-unavailable-color, var(--error-color, #b00020));
|
|
406
|
+
opacity: 0.85;
|
|
407
|
+
pointer-events: none;
|
|
408
|
+
z-index: 2;
|
|
409
|
+
}
|
|
410
|
+
`;
|
|
411
|
+
|
|
412
|
+
/** Badge template partial: renders nothing while available. */
|
|
413
|
+
export function availabilityBadge(available) {
|
|
414
|
+
return available ? '' : html`<span class="feezal-unavail-badge" title="Device unavailable">⚠</span>`;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* E117: shared descriptor for the `publish-local` attribute — spread into an
|
|
419
|
+
* element's `feezal.attributes` right after `publish` so the label and help
|
|
420
|
+
* text can never drift between elements. The element declares
|
|
421
|
+
* `publishLocal: {type: Boolean, reflect: true, attribute: 'publish-local'}`
|
|
422
|
+
* and passes `{local: this.publishLocal}` as the pub() options; the
|
|
423
|
+
* connection then loops the message back page-locally instead of sending it
|
|
424
|
+
* to the broker (see FeezalConnection.pub).
|
|
425
|
+
*/
|
|
426
|
+
export const publishLocalAttribute = {
|
|
427
|
+
name: 'publish-local',
|
|
428
|
+
type: 'boolean',
|
|
429
|
+
default: false,
|
|
430
|
+
help: 'Publish page-locally instead of to the broker: the payload reaches only subscribers in THIS browser tab (dialog triggers, view switches, wiring elements together). Nothing is sent over MQTT, nothing is retained, and it works while disconnected.'
|
|
431
|
+
};
|
|
432
|
+
|
|
265
433
|
// Re-export so element files can do a single import.
|
|
266
434
|
export {html, css};
|
package/feezal-topic-input.js
CHANGED
|
@@ -141,7 +141,7 @@ class FeezalTopicInput extends LitElement {
|
|
|
141
141
|
render() {
|
|
142
142
|
return html`
|
|
143
143
|
<div class="topic-wrap">
|
|
144
|
-
<sl-input size="${this.size}" autocomplete="off"
|
|
144
|
+
<sl-input size="${this.size}" autocomplete="off" clearable
|
|
145
145
|
label="${this.label || ''}"
|
|
146
146
|
placeholder="${this.placeholder}"
|
|
147
147
|
.value="${this.value ?? ''}"
|
package/package.json
CHANGED