mirror of
https://github.com/adrianjagielak/home-assistant-futurehome.git
synced 2025-09-13 15:47:08 +00:00
Add support for 'barrier_ctrl' service
This commit is contained in:
parent
dcab7a7112
commit
58e4ea228c
@ -7,6 +7,7 @@
|
|||||||
- Do not expose 'battery' entity twice if it supports both level and low/high binary state.
|
- Do not expose 'battery' entity twice if it supports both level and low/high binary state.
|
||||||
- Changed the default 'sensor_lumin' unit from 'Lux' to 'lx'.
|
- Changed the default 'sensor_lumin' unit from 'Lux' to 'lx'.
|
||||||
- Added support for 'indicator_ctrl' service (identify devices).
|
- Added support for 'indicator_ctrl' service (identify devices).
|
||||||
|
- Added support for 'barrier_ctrl' service (devices like garage doors, barriers, and window shades).
|
||||||
|
|
||||||
# 0.1.0 (24.07.2025)
|
# 0.1.0 (24.07.2025)
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ todo: links to the .ts service implementations below
|
|||||||
| Service | Example device | Implementation status |
|
| Service | Example device | Implementation status |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| _alarms_ services | [Brannvarsler](https://www.futurehome.io/en_no/shop/brannvarsler-230v) | |
|
| _alarms_ services | [Brannvarsler](https://www.futurehome.io/en_no/shop/brannvarsler-230v) | |
|
||||||
| barrier_ctrl | | |
|
| barrier_ctrl | | ✅ |
|
||||||
| basic | | ✅ |
|
| basic | | ✅ |
|
||||||
| battery | | ✅ |
|
| battery | | ✅ |
|
||||||
| chargepoint | [Futurehome Charge](https://www.futurehome.io/en_no/shop/charge) | |
|
| chargepoint | [Futurehome Charge](https://www.futurehome.io/en_no/shop/charge) | |
|
||||||
|
@ -24,10 +24,48 @@ export type VinculumPd7Device = {
|
|||||||
| null;
|
| null;
|
||||||
services?: Record<string, VinculumPd7Service> | null;
|
services?: Record<string, VinculumPd7Service> | null;
|
||||||
type?: {
|
type?: {
|
||||||
// User-defined device type (e.g. "sensor", "chargepoint", or "light")
|
// User-defined device type
|
||||||
type?: string | null;
|
type?:
|
||||||
// User-defined device subtype (e.g. "presence" or "car_charger")
|
| 'appliance'
|
||||||
subtype?: string | null;
|
| 'battery'
|
||||||
|
| 'blinds'
|
||||||
|
| 'boiler'
|
||||||
|
| 'chargepoint'
|
||||||
|
| 'door_lock'
|
||||||
|
| 'fan'
|
||||||
|
| 'fire_detector'
|
||||||
|
| 'garage_door'
|
||||||
|
| 'gas_detector'
|
||||||
|
| 'gate'
|
||||||
|
| 'heat_detector'
|
||||||
|
| 'heat_pump'
|
||||||
|
| 'heater'
|
||||||
|
| 'leak_detector'
|
||||||
|
| 'light'
|
||||||
|
| 'media_player'
|
||||||
|
| 'meter'
|
||||||
|
| 'sensor'
|
||||||
|
| 'siren'
|
||||||
|
| 'thermostat'
|
||||||
|
| 'input'
|
||||||
|
| 'water_valve'
|
||||||
|
| string
|
||||||
|
| null;
|
||||||
|
// User-defined device subtype
|
||||||
|
subtype?:
|
||||||
|
| 'car_charger'
|
||||||
|
| 'door'
|
||||||
|
| 'door_lock'
|
||||||
|
| 'garage'
|
||||||
|
| 'lock'
|
||||||
|
| 'main_elec'
|
||||||
|
| 'presence'
|
||||||
|
| 'scene'
|
||||||
|
| 'window'
|
||||||
|
| 'window_lock'
|
||||||
|
| 'inverter'
|
||||||
|
| string
|
||||||
|
| null;
|
||||||
} | null;
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1 +1,14 @@
|
|||||||
export type EntityCategory = undefined | 'config' | 'diagnostic';
|
export type EntityCategory = undefined | 'config' | 'diagnostic';
|
||||||
|
|
||||||
|
export type CoverDeviceClass =
|
||||||
|
| 'awning'
|
||||||
|
| 'blind'
|
||||||
|
| 'curtain'
|
||||||
|
| 'damper'
|
||||||
|
| 'door'
|
||||||
|
| 'garage'
|
||||||
|
| 'gate'
|
||||||
|
| 'shade'
|
||||||
|
| 'shutter'
|
||||||
|
| 'window'
|
||||||
|
| null;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { CoverDeviceClass } from './_enums';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a MQTT Cover component for Home Assistant MQTT Discovery.
|
* Represents a MQTT Cover component for Home Assistant MQTT Discovery.
|
||||||
*
|
*
|
||||||
@ -38,9 +40,9 @@ export interface CoverComponent {
|
|||||||
/**
|
/**
|
||||||
* Sets the [class of the device](https://www.home-assistant.io/integrations/cover/#device_class),
|
* Sets the [class of the device](https://www.home-assistant.io/integrations/cover/#device_class),
|
||||||
* changing the device state and icon that is displayed on the frontend.
|
* changing the device state and icon that is displayed on the frontend.
|
||||||
* The `device_class` can be `null`.
|
* The `device_class` can be `null` (generic cover).
|
||||||
*/
|
*/
|
||||||
device_class?: string | null;
|
device_class?: CoverDeviceClass;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flag which defines if the entity should be enabled when first added.
|
* Flag which defines if the entity should be enabled when first added.
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
VinculumPd7Service,
|
VinculumPd7Service,
|
||||||
} from '../fimp/vinculum_pd7_device';
|
} from '../fimp/vinculum_pd7_device';
|
||||||
import { log } from '../logger';
|
import { log } from '../logger';
|
||||||
|
import { barrier_ctrl__components } from '../services/barrier_ctrl';
|
||||||
import { basic__components } from '../services/basic';
|
import { basic__components } from '../services/basic';
|
||||||
import { battery__components } from '../services/battery';
|
import { battery__components } from '../services/battery';
|
||||||
import { color_ctrl__components } from '../services/color_ctrl';
|
import { color_ctrl__components } from '../services/color_ctrl';
|
||||||
@ -158,6 +159,7 @@ const serviceHandlers: {
|
|||||||
svc: VinculumPd7Service,
|
svc: VinculumPd7Service,
|
||||||
) => ServiceComponentsCreationResult | undefined;
|
) => ServiceComponentsCreationResult | undefined;
|
||||||
} = {
|
} = {
|
||||||
|
barrier_ctrl: barrier_ctrl__components,
|
||||||
basic: basic__components,
|
basic: basic__components,
|
||||||
battery: battery__components,
|
battery: battery__components,
|
||||||
color_ctrl: color_ctrl__components,
|
color_ctrl: color_ctrl__components,
|
||||||
|
@ -238,7 +238,8 @@ import { delay } from './utils';
|
|||||||
case 'evt.presence.report':
|
case 'evt.presence.report':
|
||||||
case 'evt.scene.report':
|
case 'evt.scene.report':
|
||||||
case 'evt.sensor.report':
|
case 'evt.sensor.report':
|
||||||
case 'evt.setpoint.report': {
|
case 'evt.setpoint.report':
|
||||||
|
case 'evt.state.report': {
|
||||||
haUpdateStateSensorReport({
|
haUpdateStateSensorReport({
|
||||||
topic,
|
topic,
|
||||||
value: msg.val,
|
value: msg.val,
|
||||||
|
172
futurehome/src/services/barrier_ctrl.ts
Normal file
172
futurehome/src/services/barrier_ctrl.ts
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import { sendFimpMsg } from '../fimp/fimp';
|
||||||
|
import {
|
||||||
|
VinculumPd7Device,
|
||||||
|
VinculumPd7Service,
|
||||||
|
} from '../fimp/vinculum_pd7_device';
|
||||||
|
import { HaMqttComponent } from '../ha/mqtt_components/_component';
|
||||||
|
import { CoverDeviceClass } from '../ha/mqtt_components/_enums';
|
||||||
|
import { CoverComponent } from '../ha/mqtt_components/cover';
|
||||||
|
import {
|
||||||
|
CommandHandlers,
|
||||||
|
ServiceComponentsCreationResult,
|
||||||
|
} from '../ha/publish_device';
|
||||||
|
|
||||||
|
export function barrier_ctrl__components(
|
||||||
|
topicPrefix: string,
|
||||||
|
device: VinculumPd7Device,
|
||||||
|
svc: VinculumPd7Service,
|
||||||
|
): ServiceComponentsCreationResult | undefined {
|
||||||
|
const components: Record<string, HaMqttComponent> = {};
|
||||||
|
const commandHandlers: CommandHandlers = {};
|
||||||
|
|
||||||
|
// Main cover component
|
||||||
|
const commandTopic = `${topicPrefix}${svc.addr}/command`;
|
||||||
|
const positionCommandTopic = `${topicPrefix}${svc.addr}/set_position`;
|
||||||
|
const stopCommandTopic = `${topicPrefix}${svc.addr}/stop`;
|
||||||
|
|
||||||
|
// Determine device class based on device type/functionality
|
||||||
|
let deviceClass: CoverDeviceClass;
|
||||||
|
if (
|
||||||
|
device.type?.type === 'garage_door' ||
|
||||||
|
device.functionality === 'security'
|
||||||
|
) {
|
||||||
|
deviceClass = 'garage';
|
||||||
|
} else if (device.type?.type === 'gate') {
|
||||||
|
deviceClass = 'gate';
|
||||||
|
} else if (
|
||||||
|
device.type?.type === 'blinds' ||
|
||||||
|
device.functionality === 'shading'
|
||||||
|
) {
|
||||||
|
deviceClass = 'blind';
|
||||||
|
} else {
|
||||||
|
deviceClass = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if position control is supported
|
||||||
|
const supportsPosition = svc.props?.sup_tposition === true;
|
||||||
|
const supportedTargetStates = svc.props?.sup_tstates || [];
|
||||||
|
|
||||||
|
const coverComponent: CoverComponent = {
|
||||||
|
unique_id: svc.addr,
|
||||||
|
platform: 'cover',
|
||||||
|
device_class: deviceClass,
|
||||||
|
command_topic: commandTopic,
|
||||||
|
optimistic: false,
|
||||||
|
value_template: `{{ value_json['${svc.addr}'].state }}`,
|
||||||
|
// Standard Home Assistant cover payloads
|
||||||
|
payload_open: 'OPEN',
|
||||||
|
payload_close: 'CLOSE',
|
||||||
|
payload_stop: 'STOP',
|
||||||
|
state_open: 'open',
|
||||||
|
state_closed: 'closed',
|
||||||
|
state_opening: 'opening',
|
||||||
|
state_closing: 'closing',
|
||||||
|
state_stopped: 'stopped',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add position support if available
|
||||||
|
if (supportsPosition) {
|
||||||
|
coverComponent.set_position_topic = positionCommandTopic;
|
||||||
|
coverComponent.position_template = `{{ value_json['${svc.addr}'].position | default(0) }}`;
|
||||||
|
coverComponent.position_closed = 0;
|
||||||
|
coverComponent.position_open = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add stop command if supported
|
||||||
|
if (svc.intf?.includes('cmd.op.stop')) {
|
||||||
|
coverComponent.command_topic = stopCommandTopic;
|
||||||
|
}
|
||||||
|
|
||||||
|
components[svc.addr] = coverComponent;
|
||||||
|
|
||||||
|
// Command handlers
|
||||||
|
commandHandlers[commandTopic] = async (payload: string) => {
|
||||||
|
let targetState: string;
|
||||||
|
|
||||||
|
switch (payload) {
|
||||||
|
case 'OPEN':
|
||||||
|
targetState = 'open';
|
||||||
|
break;
|
||||||
|
case 'CLOSE':
|
||||||
|
targetState = 'closed';
|
||||||
|
break;
|
||||||
|
case 'STOP':
|
||||||
|
if (svc.intf?.includes('cmd.op.stop')) {
|
||||||
|
await sendFimpMsg({
|
||||||
|
address: svc.addr,
|
||||||
|
service: 'barrier_ctrl',
|
||||||
|
cmd: 'cmd.op.stop',
|
||||||
|
val_t: 'null',
|
||||||
|
val: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only send target state if it's supported
|
||||||
|
if (supportedTargetStates.includes(targetState)) {
|
||||||
|
await sendFimpMsg({
|
||||||
|
address: svc.addr,
|
||||||
|
service: 'barrier_ctrl',
|
||||||
|
cmd: 'cmd.tstate.set',
|
||||||
|
val_t: 'string',
|
||||||
|
val: targetState,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Position command handler (if position control is supported)
|
||||||
|
if (supportsPosition) {
|
||||||
|
commandHandlers[positionCommandTopic] = async (payload: string) => {
|
||||||
|
const position = parseInt(payload, 10);
|
||||||
|
if (Number.isNaN(position) || position < 0 || position > 100) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine target state based on position
|
||||||
|
let targetState: string;
|
||||||
|
if (position === 0) {
|
||||||
|
targetState = 'closed';
|
||||||
|
} else if (position === 100) {
|
||||||
|
targetState = 'open';
|
||||||
|
} else {
|
||||||
|
// For partial positions, we'll use 'open' as the target state
|
||||||
|
targetState = 'open';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only send if target state is supported
|
||||||
|
if (supportedTargetStates.includes(targetState)) {
|
||||||
|
await sendFimpMsg({
|
||||||
|
address: svc.addr,
|
||||||
|
service: 'barrier_ctrl',
|
||||||
|
cmd: 'cmd.tstate.set',
|
||||||
|
val_t: 'string',
|
||||||
|
val: targetState,
|
||||||
|
props: {
|
||||||
|
position: position.toString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop command handler (separate topic if needed)
|
||||||
|
if (svc.intf?.includes('cmd.op.stop')) {
|
||||||
|
commandHandlers[stopCommandTopic] = async (_payload: string) => {
|
||||||
|
await sendFimpMsg({
|
||||||
|
address: svc.addr,
|
||||||
|
service: 'barrier_ctrl',
|
||||||
|
cmd: 'cmd.op.stop',
|
||||||
|
val_t: 'null',
|
||||||
|
val: null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
components,
|
||||||
|
commandHandlers,
|
||||||
|
};
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user