Skip to content

Commit

Permalink
Door state and volume units and new detection features (#88)
Browse files Browse the repository at this point in the history
* Door state and volume units

* Added door state for device type "lock"
* Added default unit for volume states

* Enhance tests

* Combine two separate "ON" detections

* Combine two separate "ON" detections

* Rest

* remove debug log

* prepare
  • Loading branch information
Apollon77 authored Jan 31, 2025
1 parent 321d88d commit 2173aec
Show file tree
Hide file tree
Showing 9 changed files with 1,243 additions and 87 deletions.
34 changes: 17 additions & 17 deletions DEVICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -432,19 +432,20 @@ In [brackets] is given the class name of device.


### Lock [lock]
| R | Name | Role | Unit | Type | Wr | Ind | Mult | Regex |
|---|-----------|-------------------------------|------|---------|----|-----|------|------------------------------------------------------------------------------|
| * | SET | switch.lock | | boolean | W | | | `/^switch\.lock$/` |
| | ACTUAL | state | | boolean | - | | | `/^state$/` |
| | OPEN | button | | boolean | W | | | |
| | DIRECTION | indicator.direction | | boolean | | X | | `/^indicator\.direction$/` |
| | DIRECTION | value.direction | | number | | | | `/^(indicator|value)\.direction$/` |
| | WORKING | indicator.working | | | | X | | `/^indicator\.working$/` |
| | UNREACH | indicator.maintenance.unreach | | boolean | | X | | `/^indicator(\.maintenance)?\.unreach$/` |
| | LOWBAT | indicator.maintenance.lowbat | | boolean | | X | | `/^indicator(\.maintenance)?\.lowbat$|^indicator(\.maintenance)?\.battery$/` |
| | MAINTAIN | indicator.maintenance | | boolean | | X | | `/^indicator\.maintenance$/` |
| | ERROR | indicator.error | | | | X | | `/^indicator\.error$/` |
| | BATTERY | value.battery | % | number | - | | | `/^value\.battery$/` |
| R | Name | Role | Unit | Type | Wr | Ind | Mult | Regex |
|---|------------|-------------------------------|------|---------|----|-----|------|------------------------------------------------------------------------------|
| * | SET | switch.lock | | boolean | W | | | `/^switch\.lock$/` |
| | ACTUAL | state | | boolean | - | | | `/^state$/` |
| | OPEN | button | | boolean | W | | | |
| | DOOR_STATE | sensor.door | | boolean | - | | | `/^state?$|^state(\.door)?$|^sensor(\.door)?/` |
| | DIRECTION | indicator.direction | | boolean | | X | | `/^indicator\.direction$/` |
| | DIRECTION | value.direction | | number | | | | `/^(indicator|value)\.direction$/` |
| | WORKING | indicator.working | | | | X | | `/^indicator\.working$/` |
| | UNREACH | indicator.maintenance.unreach | | boolean | | X | | `/^indicator(\.maintenance)?\.unreach$/` |
| | LOWBAT | indicator.maintenance.lowbat | | boolean | | X | | `/^indicator(\.maintenance)?\.lowbat$|^indicator(\.maintenance)?\.battery$/` |
| | MAINTAIN | indicator.maintenance | | boolean | | X | | `/^indicator\.maintenance$/` |
| | ERROR | indicator.error | | | | X | | `/^indicator\.error$/` |
| | BATTERY | value.battery | % | number | - | | | `/^value\.battery$/` |


### Media [mediaPlayer]
Expand Down Expand Up @@ -503,8 +504,7 @@ In [brackets] is given the class name of device.
| | BRIGHTNESS | | | number | W | | | `/^level\.brightness$/` |
| | SATURATION | level.color.saturation | % | number | W | | | `/^level\.color\.saturation$/` |
| | TEMPERATURE | level.color.temperature | °K | number | W | | | `/^level\.color\.temperature$/` |
| | ON | switch.light | | boolean | W | | | `/^switch\.light$/` |
| | ON | | | boolean | W | | | `/^switch$/` |
| | ON | switch.light | | boolean | W | | | `/^switch(\.light)?$|^state$/` |
| | ON_ACTUAL | sensor.light | | boolean | - | | | `/^(state|switch|sensor)\.light|switch$/` |
| | TRANSITION_TIME | time.span | ms | number | W | | | `/^time(\.span|\.interval)$/` |
| | ELECTRIC_POWER | value.power | W | number | - | | | `/^value\.power$/` |
Expand Down Expand Up @@ -647,8 +647,8 @@ In [brackets] is given the class name of device.
### Volume [volume]
| R | Name | Role | Unit | Type | Wr | Min | Max | Ind | Mult | Regex |
|---|----------|-------------------------------|------|---------|----|-----|-----|-----|------|------------------------------------------------------------------------------|
| * | SET | level.volume | | number | W | m | M | | | `/^level\.volume$/` |
| | ACTUAL | value.volume | | number | - | m | M | | | `/^value\.volume$/` |
| * | SET | level.volume | % | number | W | m | M | | | `/^level\.volume$/` |
| | ACTUAL | value.volume | % | number | - | m | M | | | `/^value\.volume$/` |
| | MUTE | media.mute | | boolean | W | | | | | `/^media\.mute$/` |
| | WORKING | indicator.working | | | | | | X | | `/^indicator\.working$/` |
| | UNREACH | indicator.maintenance.unreach | | boolean | | | | X | | `/^indicator(\.maintenance)?\.unreach$/` |
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,12 @@ if (controls) {

## Changelog
### **WORK IN PROGRESS**

- (@GermanBluefox) Added detection of `level.direction`
- (@Apollon77) Added door state for device type "lock"
- (@Apollon77) Added default unit for volume states
- (@Apollon77) Added new option ignoreEnums to execute the detection without enums-matching logic
- (@Apollon77) Added new option detectAllPossibleDevices to detect multiple devices in one run without checking for already used IDs
- (@Garfonso) Added missing default roles and units for saturation

### 4.1.1 (2024-12-15)
- (@Apollon77) Fixed default unit for Illuminance to "lux"
Expand Down
93 changes: 67 additions & 26 deletions src/ChannelDetector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
**/
*/

import {
type DetectOptions,
Expand Down Expand Up @@ -48,6 +48,7 @@ export class ChannelDetector {
objects: Record<string, ioBroker.Object>,
id: string,
statePattern: InternalDetectorState,
ignoreEnums: boolean,
): boolean {
if (objects[id] && objects[id].common) {
let role = null;
Expand Down Expand Up @@ -141,7 +142,7 @@ export class ChannelDetector {

if (statePattern.enums && typeof statePattern.enums === 'function') {
const enums = this._getEnumsForId(objects, id);
if (!statePattern.enums(objects[id], enums || [])) {
if (!ignoreEnums && !statePattern.enums(objects[id], enums || [])) {
return false;
}
}
Expand Down Expand Up @@ -190,19 +191,20 @@ export class ChannelDetector {
}

private _testOneState(context: DetectorContext): boolean {
const objects: Record<string, ioBroker.Object> = context.objects;
const pattern: Types = context.pattern;
const state: InternalDetectorState = context.state;
const channelStates: string[] = context.channelStates;
const usedIds: string[] = context.usedIds;
const usedInCurrentDevice: string[] = context.usedInCurrentDevice;
const ignoreIndicators: string[] = context.ignoreIndicators;
const objects = context.objects;
const pattern = context.pattern;
const state = context.state;
const channelStates = context.channelStates;
const usedIds = context.usedIds;
const usedInCurrentDevice = context.usedInCurrentDevice;
const ignoreIndicators = context.ignoreIndicators;
const ignoreEnums = context.ignoreEnums;
let result: PatternControl | null = context.result;
let found = false;
// let count = 0;

// check every state in channel
channelStates.forEach(_id => {
for (const _id of channelStates) {
// this is only valid if no one state could be multiple
// if (result && count >= result.states.length) {
// // do not look for more states as all possible found
Expand All @@ -211,7 +213,7 @@ export class ChannelDetector {

// one exception: if we already found a state with name COVER, so ignore the second one
if (state.name === 'COVER' && result?.states.find(e => e.id && e.name === 'COVER')) {
return;
continue;
}

if (state.indicator && ignoreIndicators) {
Expand All @@ -220,15 +222,15 @@ export class ChannelDetector {

if (lastStateName && ignoreIndicators.includes(lastStateName)) {
// console.log(`${_id} ignored`);
return;
continue;
}
}

if (
(state.indicator ||
(!usedInCurrentDevice.includes(_id) && // not used in a current device and pattern
(state.notSingle || !usedIds.includes(_id)))) && // or not used globally
this._applyPattern(objects, _id, state)
this._applyPattern(objects, _id, state, ignoreEnums)
) {
if (!state.indicator) {
usedInCurrentDevice.push(_id);
Expand Down Expand Up @@ -269,7 +271,7 @@ export class ChannelDetector {
(state.indicator ||
(!usedInCurrentDevice.includes(cid) &&
(state.notSingle || !usedIds.includes(cid)))) &&
this._applyPattern(objects, cid, state)
this._applyPattern(objects, cid, state, ignoreEnums)
) {
if (!state.indicator) {
usedInCurrentDevice.push(cid);
Expand All @@ -290,7 +292,7 @@ export class ChannelDetector {
}
}
}
});
}
return found;
}

Expand Down Expand Up @@ -377,10 +379,10 @@ export class ChannelDetector {

private _detectNext(options: DetectOptions): PatternControl | null {
const objects = options.objects;
const id: string = options.id;
const keys: string[] = options._keysOptional || [];
let usedIds: string[] = options._usedIdsOptional || [];
const ignoreIndicators: string[] | undefined = options.ignoreIndicators;
const id = options.id;
const keys = options._keysOptional || [];
let usedIds = options._usedIdsOptional || [];
const ignoreIndicators = options.ignoreIndicators;

if (!usedIds) {
usedIds = [];
Expand All @@ -390,6 +392,9 @@ export class ChannelDetector {
if (!objects[id] || !objects[id].common) {
return null;
}
if (options._checkedPatterns === undefined) {
options._checkedPatterns = [];
}

const context: DetectorContext = {
objects,
Expand All @@ -400,18 +405,23 @@ export class ChannelDetector {
pattern: Types.unknown,
usedInCurrentDevice: [],
state: {} as InternalDetectorState,
ignoreEnums: !!options.ignoreEnums,
};

for (const pattern in patterns) {
if (!ChannelDetector.patternIsAllowed(patterns[pattern], options.allowedTypes, options.excludedTypes)) {
if (
options._checkedPatterns.includes(pattern as Types) ||
!ChannelDetector.patternIsAllowed(patterns[pattern], options.allowedTypes, options.excludedTypes)
) {
continue;
}
options._checkedPatterns.push(pattern as Types);

context.result = null;

context.pattern = pattern as Types;
context.usedInCurrentDevice = [];
patterns[pattern].states.forEach(state => {
for (const state of patterns[pattern].states) {
let found = false;

// one of the following
Expand All @@ -421,9 +431,9 @@ export class ChannelDetector {
}
if (state.required && !found) {
context.result = null;
return false;
break;
}
});
}

if (!ChannelDetector.allRequiredStatesFound(context)) {
continue;
Expand All @@ -446,7 +456,12 @@ export class ChannelDetector {
context.result?.states.forEach((state, i) => {
if (!state.id && (state.indicator || state.searchInParent) && !state.noDeviceDetection) {
if (
this._applyPattern(objects, _id, state.original as InternalDetectorState) &&
this._applyPattern(
objects,
_id,
state.original as InternalDetectorState,
!!options.ignoreEnums,
) &&
context.result
) {
context.result.states[i].id = _id;
Expand Down Expand Up @@ -491,8 +506,19 @@ export class ChannelDetector {
let _usedIdsOptional = options._usedIdsOptional;
// let ignoreIndicators = options.ignoreIndicators;

if (this.cache[id] !== undefined) {
return this.cache[id];
if (!options.ignoreCache && this.cache[id]) {
// We validate if the cache matches the requirements and if not skip the cache
if (!options.allowedTypes && !options.excludedTypes) {
return this.cache[id];
}
const allowedTypes = options.allowedTypes ?? [];
const excludedTypes = options.excludedTypes ?? [];
const result = this.cache[id].filter(
({ type }) => allowedTypes.includes(type) && !excludedTypes.includes(type),
);
if (result.length) {
return result;
}
}

if (!_keysOptional) {
Expand All @@ -506,11 +532,26 @@ export class ChannelDetector {
options._usedIdsOptional = _usedIdsOptional;
}

// When we do want to detect a special device type we can ignore enums
if (options.ignoreEnums === undefined && options.allowedTypes && options.allowedTypes.length === 1) {
options.ignoreEnums = true;
}
if (options.detectAllPossibleDevices) {
options.excludedTypes = options.excludedTypes || [];
if (!options.excludedTypes.includes(Types.info)) {
options.excludedTypes.push(Types.info);
}
}
options._checkedPatterns = [];

const result = [];
let detected;

while ((detected = this._detectNext(options))) {
result.push(detected);
if (options.detectAllPossibleDevices) {
options._usedIdsOptional = [];
}
}

this.cache[id] = result.length ? result : null;
Expand Down
21 changes: 12 additions & 9 deletions src/TypePatterns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -847,22 +847,14 @@ export const patterns: { [key: string]: InternalPatternControl } = {
defaultUnit: '°K',
},
{
role: /^switch\.light$/,
role: /^switch(\.light)?$|^state$/,
indicator: false,
type: StateType.Boolean,
write: true,
name: 'ON',
required: false,
defaultRole: 'switch.light',
},
{
role: /^switch$/,
indicator: false,
type: StateType.Boolean,
write: true,
name: 'ON',
required: false,
},
{
role: /^(state|switch|sensor)\.light|switch$/,
indicator: false,
Expand Down Expand Up @@ -2319,6 +2311,15 @@ export const patterns: { [key: string]: InternalPatternControl } = {
noSubscribe: true,
defaultRole: 'button',
},
{
role: /^state?$|^state(\.door)?$|^sensor(\.door)?/,
indicator: false,
type: StateType.Boolean,
write: false,
name: 'DOOR_STATE',
required: false,
defaultRole: 'sensor.door',
},
SharedPatterns.direction,
SharedPatterns.direction_enum,
SharedPatterns.working,
Expand Down Expand Up @@ -2572,6 +2573,7 @@ export const patterns: { [key: string]: InternalPatternControl } = {
name: 'SET',
required: true,
defaultRole: 'level.volume',
defaultUnit: '%',
},
// optional
{
Expand All @@ -2584,6 +2586,7 @@ export const patterns: { [key: string]: InternalPatternControl } = {
name: 'ACTUAL',
required: false,
defaultRole: 'value.volume',
defaultUnit: '%',
},
{
role: /^media\.mute$/,
Expand Down
Loading

0 comments on commit 2173aec

Please sign in to comment.