mirror of
https://github.com/adrianjagielak/home-assistant-futurehome.git
synced 2025-09-13 07:37:09 +00:00
Add support for 'media_player' service
This commit is contained in:
parent
c84c2744e0
commit
1807b5157b
@ -67,11 +67,11 @@ todo: links to the .ts service implementations below
|
||||
| chargepoint | [Futurehome Charge](https://www.futurehome.io/en_no/shop/charge) | ✅ |
|
||||
| color_ctrl | | ✅ |
|
||||
| complex_alarm_system | | |
|
||||
| door_lock | | |
|
||||
| door_lock | | |
|
||||
| doorman | | |
|
||||
| fan_ctrl | | ✅ |
|
||||
| light | | ✅ |
|
||||
| media_player | | |
|
||||
| media_player | | ✅ |
|
||||
| meter_elec | [HAN-Sensor](https://www.futurehome.io/en/shop/han-sensor) | |
|
||||
| out_bin_switch | [16A Puck Relé](https://www.futurehome.io/en_no/shop/puck-relay-16a) | ✅ |
|
||||
| out_lvl_switch | [Smart LED Dimmer](https://www.futurehome.io/en_no/shop/smart-led-dimmer-polar-white) | ✅ |
|
||||
|
@ -1,6 +1,10 @@
|
||||
<!-- https://developers.home-assistant.io/docs/add-ons/presentation#keeping-a-changelog -->
|
||||
|
||||
|
||||
## 0.1.4 (25.07.2025)
|
||||
|
||||
- Added support for 'media_player' service.
|
||||
|
||||
## 0.1.3 (25.07.2025)
|
||||
|
||||
- Added support for 'chargepoint' service (EV chargers).
|
||||
|
@ -66,11 +66,11 @@ todo: links to the .ts service implementations below
|
||||
| chargepoint | [Futurehome Charge](https://www.futurehome.io/en_no/shop/charge) | ✅ |
|
||||
| color_ctrl | | ✅ |
|
||||
| complex_alarm_system | | |
|
||||
| door_lock | | |
|
||||
| door_lock | | |
|
||||
| doorman | | |
|
||||
| fan_ctrl | | ✅ |
|
||||
| light | | ✅ |
|
||||
| media_player | | |
|
||||
| media_player | | ✅ |
|
||||
| meter_elec | [HAN-Sensor](https://www.futurehome.io/en/shop/han-sensor) | |
|
||||
| out_bin_switch | [16A Puck Relé](https://www.futurehome.io/en_no/shop/puck-relay-16a) | ✅ |
|
||||
| out_lvl_switch | [Smart LED Dimmer](https://www.futurehome.io/en_no/shop/smart-led-dimmer-polar-white) | ✅ |
|
||||
|
@ -1,6 +1,6 @@
|
||||
# https://developers.home-assistant.io/docs/add-ons/configuration#add-on-config
|
||||
name: Futurehome
|
||||
version: "0.1.3"
|
||||
version: "0.1.4"
|
||||
slug: futurehome
|
||||
description: Local Futurehome Smarthub integration
|
||||
url: "https://github.com/adrianjagielak/home-assistant-futurehome"
|
||||
|
@ -11,6 +11,7 @@ import { chargepoint__components } from '../services/chargepoint';
|
||||
import { color_ctrl__components } from '../services/color_ctrl';
|
||||
import { fan_ctrl__components } from '../services/fan_ctrl';
|
||||
import { indicator_ctrl__components } from '../services/indicator_ctrl';
|
||||
import { media_player__components } from '../services/media_player';
|
||||
import { out_bin_switch__components } from '../services/out_bin_switch';
|
||||
import { out_lvl_switch__components } from '../services/out_lvl_switch';
|
||||
import { scene_ctrl__components } from '../services/scene_ctrl';
|
||||
@ -54,6 +55,7 @@ import { sensor_watflow__components } from '../services/sensor_watflow';
|
||||
import { sensor_watpressure__components } from '../services/sensor_watpressure';
|
||||
import { sensor_wattemp__components } from '../services/sensor_wattemp';
|
||||
import { sensor_weight__components } from '../services/sensor_weight';
|
||||
import { sensor_wind__components } from '../services/sensor_wind';
|
||||
import { thermostat__components } from '../services/thermostat';
|
||||
import { water_heater__components } from '../services/water_heater';
|
||||
import { abbreviateHaMqttKeys } from './abbreviate_ha_mqtt_keys';
|
||||
@ -168,6 +170,7 @@ const serviceHandlers: {
|
||||
color_ctrl: color_ctrl__components,
|
||||
fan_ctrl: fan_ctrl__components,
|
||||
indicator_ctrl: indicator_ctrl__components,
|
||||
media_player: media_player__components,
|
||||
out_bin_switch: out_bin_switch__components,
|
||||
out_lvl_switch: out_lvl_switch__components,
|
||||
scene_ctrl: scene_ctrl__components,
|
||||
@ -211,6 +214,7 @@ const serviceHandlers: {
|
||||
sensor_watpressure: sensor_watpressure__components,
|
||||
sensor_wattemp: sensor_wattemp__components,
|
||||
sensor_weight: sensor_weight__components,
|
||||
sensor_wind: sensor_wind__components,
|
||||
thermostat: thermostat__components,
|
||||
water_heater: water_heater__components,
|
||||
};
|
||||
|
@ -1942,5 +1942,58 @@
|
||||
}
|
||||
},
|
||||
"metadata": null
|
||||
},
|
||||
{
|
||||
"client": {
|
||||
"name": "Smart Speaker"
|
||||
},
|
||||
"id": 1001,
|
||||
"model": "zigbee - Futurehome - Smart Speaker",
|
||||
"modelAlias": "Smart Speaker",
|
||||
"type": {
|
||||
"subtype": null,
|
||||
"supported": {
|
||||
"boiler": []
|
||||
},
|
||||
"type": null
|
||||
},
|
||||
"locationRef": {
|
||||
"id": "11",
|
||||
"type": "Room"
|
||||
},
|
||||
"thingRef": {
|
||||
"id": "166"
|
||||
},
|
||||
"origin": "056e610614c848c50b",
|
||||
"services": {
|
||||
"media_player": {
|
||||
"name": "media_player",
|
||||
"addr": "/rt:dev/rn:zigbee/ad:1/sv:media_player/ad:1001_0",
|
||||
"enabled": true,
|
||||
"props": {
|
||||
"sup_playback": ["play", "pause", "next_track", "previous_track"],
|
||||
"sup_modes": ["repeat", "repeat_one", "shuffle", "crossfade"],
|
||||
"sup_metadata": ["album", "track", "artist", "image_url"]
|
||||
},
|
||||
"intf": [
|
||||
"cmd.playback.set",
|
||||
"cmd.playback.get_report",
|
||||
"evt.playback.report",
|
||||
"cmd.playbackmode.set",
|
||||
"cmd.playbackmode.get_report",
|
||||
"evt.playbackmode.report",
|
||||
"cmd.volume.set",
|
||||
"cmd.volume.get_report",
|
||||
"evt.volume.report",
|
||||
"cmd.mute.set",
|
||||
"cmd.mute.get_report",
|
||||
"evt.mute.report",
|
||||
"cmd.metadata.get_report",
|
||||
"evt.metadata.report"
|
||||
],
|
||||
"metadata": null
|
||||
}
|
||||
},
|
||||
"metadata": null
|
||||
}
|
||||
]
|
||||
|
@ -1243,5 +1243,76 @@
|
||||
"name": "sensor_wattemp"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 1001,
|
||||
"services": [
|
||||
{
|
||||
"addr": "/rt:dev/rn:zigbee/ad:1/sv:media_player/ad:1001_0",
|
||||
"attributes": [
|
||||
{
|
||||
"name": "playback",
|
||||
"values": [
|
||||
{
|
||||
"ts": "2022-05-05 00:01:50 +0200",
|
||||
"val": "play",
|
||||
"val_t": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "playbackmode",
|
||||
"values": [
|
||||
{
|
||||
"ts": "2022-05-05 00:01:50 +0200",
|
||||
"val": {
|
||||
"repeat": true,
|
||||
"repeat_one": false,
|
||||
"shuffle": false,
|
||||
"crossfade": false
|
||||
},
|
||||
"val_t": "bool_map"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "volume",
|
||||
"values": [
|
||||
{
|
||||
"ts": "2022-05-05 00:01:50 +0200",
|
||||
"val": 69,
|
||||
"val_t": "int"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "mute",
|
||||
"values": [
|
||||
{
|
||||
"ts": "2022-05-05 00:01:50 +0200",
|
||||
"val": false,
|
||||
"val_t": "bool"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "metadata",
|
||||
"values": [
|
||||
{
|
||||
"ts": "2022-05-05 00:01:50 +0200",
|
||||
"val": {
|
||||
"album": "The Dark Side of the Moon",
|
||||
"track": "Money",
|
||||
"artist": "Pink Floyd",
|
||||
"image_url": "https://upload.wikimedia.org/wikipedia/en/3/3b/Dark_Side_of_the_Moon.png"
|
||||
},
|
||||
"val_t": "str_map"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"name": "media_player"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
226
futurehome/src/services/media_player.ts
Normal file
226
futurehome/src/services/media_player.ts
Normal file
@ -0,0 +1,226 @@
|
||||
import { sendFimpMsg } from '../fimp/fimp';
|
||||
import {
|
||||
VinculumPd7Device,
|
||||
VinculumPd7Service,
|
||||
} from '../fimp/vinculum_pd7_device';
|
||||
import { HaMqttComponent } from '../ha/mqtt_components/_component';
|
||||
import { SensorComponent } from '../ha/mqtt_components/sensor';
|
||||
import { NumberComponent } from '../ha/mqtt_components/number';
|
||||
import { SwitchComponent } from '../ha/mqtt_components/switch';
|
||||
import { SelectComponent } from '../ha/mqtt_components/select';
|
||||
import { ImageComponent } from '../ha/mqtt_components/image';
|
||||
import {
|
||||
CommandHandlers,
|
||||
ServiceComponentsCreationResult,
|
||||
} from '../ha/publish_device';
|
||||
|
||||
export function media_player__components(
|
||||
topicPrefix: string,
|
||||
device: VinculumPd7Device,
|
||||
svc: VinculumPd7Service,
|
||||
): ServiceComponentsCreationResult | undefined {
|
||||
const components: Record<string, HaMqttComponent> = {};
|
||||
const commandHandlers: CommandHandlers = {};
|
||||
|
||||
// Extract supported properties
|
||||
const supPlayback = svc.props?.sup_playback || [];
|
||||
const supModes = svc.props?.sup_modes || [];
|
||||
const supMetadata = svc.props?.sup_metadata || [];
|
||||
|
||||
// Command topics
|
||||
const playbackCommandTopic = `${topicPrefix}${svc.addr}/playback/command`;
|
||||
const volumeCommandTopic = `${topicPrefix}${svc.addr}/volume/command`;
|
||||
const muteCommandTopic = `${topicPrefix}${svc.addr}/mute/command`;
|
||||
|
||||
// --- Main playback control as a select entity ---
|
||||
if (supPlayback.length > 0) {
|
||||
const playbackComponent: SelectComponent = {
|
||||
unique_id: `${svc.addr}_playback`,
|
||||
platform: 'select',
|
||||
name: 'Playback Control',
|
||||
command_topic: playbackCommandTopic,
|
||||
options: supPlayback,
|
||||
optimistic: false,
|
||||
value_template: `{{ value_json['${svc.addr}'].playback | default('${supPlayback[0]}') }}`,
|
||||
icon: 'mdi:play-pause',
|
||||
};
|
||||
|
||||
components[`${svc.addr}_playback`] = playbackComponent;
|
||||
|
||||
// Playback command handler
|
||||
commandHandlers[playbackCommandTopic] = async (payload: string) => {
|
||||
if (!supPlayback.includes(payload)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await sendFimpMsg({
|
||||
address: svc.addr,
|
||||
service: 'media_player',
|
||||
cmd: 'cmd.playback.set',
|
||||
val_t: 'string',
|
||||
val: payload,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// --- Volume control as a number entity ---
|
||||
if (svc.intf?.includes('cmd.volume.set')) {
|
||||
const volumeComponent: NumberComponent = {
|
||||
unique_id: `${svc.addr}_volume`,
|
||||
platform: 'number',
|
||||
name: 'Volume',
|
||||
command_topic: volumeCommandTopic,
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
mode: 'slider',
|
||||
unit_of_measurement: '%',
|
||||
optimistic: false,
|
||||
value_template: `{{ value_json['${svc.addr}'].volume | default(50) }}`,
|
||||
icon: 'mdi:volume-high',
|
||||
};
|
||||
|
||||
components[`${svc.addr}_volume`] = volumeComponent;
|
||||
|
||||
// Volume command handler
|
||||
commandHandlers[volumeCommandTopic] = async (payload: string) => {
|
||||
const volume = parseInt(payload, 10);
|
||||
if (Number.isNaN(volume) || volume < 0 || volume > 100) {
|
||||
return;
|
||||
}
|
||||
|
||||
await sendFimpMsg({
|
||||
address: svc.addr,
|
||||
service: 'media_player',
|
||||
cmd: 'cmd.volume.set',
|
||||
val_t: 'int',
|
||||
val: volume,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// --- Mute control as a switch entity ---
|
||||
if (svc.intf?.includes('cmd.mute.set')) {
|
||||
const muteComponent: SwitchComponent = {
|
||||
unique_id: `${svc.addr}_mute`,
|
||||
platform: 'switch',
|
||||
name: 'Mute',
|
||||
command_topic: muteCommandTopic,
|
||||
optimistic: false,
|
||||
value_template: `{{ 'ON' if value_json['${svc.addr}'].mute else 'OFF' }}`,
|
||||
payload_on: 'true',
|
||||
payload_off: 'false',
|
||||
icon: 'mdi:volume-off',
|
||||
};
|
||||
|
||||
components[`${svc.addr}_mute`] = muteComponent;
|
||||
|
||||
// Mute command handler
|
||||
commandHandlers[muteCommandTopic] = async (payload: string) => {
|
||||
const mute = payload === 'true' || payload === 'ON';
|
||||
|
||||
await sendFimpMsg({
|
||||
address: svc.addr,
|
||||
service: 'media_player',
|
||||
cmd: 'cmd.mute.set',
|
||||
val_t: 'bool',
|
||||
val: mute,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// --- Playback mode controls as switch entities ---
|
||||
for (const mode of supModes) {
|
||||
const modeCommandTopic = `${topicPrefix}${svc.addr}/mode_${mode}/command`;
|
||||
|
||||
const modeComponent: SwitchComponent = {
|
||||
unique_id: `${svc.addr}_mode_${mode}`,
|
||||
platform: 'switch',
|
||||
name: `${mode.charAt(0).toUpperCase() + mode.slice(1)} Mode`,
|
||||
command_topic: modeCommandTopic,
|
||||
optimistic: false,
|
||||
value_template: `{{ 'ON' if value_json['${svc.addr}'].playbackmode.${mode} else 'OFF' }}`,
|
||||
payload_on: 'true',
|
||||
payload_off: 'false',
|
||||
icon: getPlaybackModeIcon(mode),
|
||||
};
|
||||
|
||||
components[`${svc.addr}_mode_${mode}`] = modeComponent;
|
||||
|
||||
// Mode command handler
|
||||
commandHandlers[modeCommandTopic] = async (payload: string) => {
|
||||
const enabled = payload === 'true' || payload === 'ON';
|
||||
|
||||
// We need to send the full playbackmode map, so we'll need to get current state
|
||||
// For now, just send the single mode change - the device should handle merging
|
||||
await sendFimpMsg({
|
||||
address: svc.addr,
|
||||
service: 'media_player',
|
||||
cmd: 'cmd.playbackmode.set',
|
||||
val_t: 'bool_map',
|
||||
val: {
|
||||
[mode]: enabled,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// --- Metadata sensors ---
|
||||
for (const metadata of supMetadata) {
|
||||
if (metadata === 'image_url') {
|
||||
// Image metadata as image entity
|
||||
const imageComponent: ImageComponent = {
|
||||
unique_id: `${svc.addr}_${metadata}`,
|
||||
platform: 'image',
|
||||
name: 'Album Art',
|
||||
url_topic: `${topicPrefix}/state`,
|
||||
url_template: `{{ value_json['${svc.addr}'].metadata.image_url | default('') }}`,
|
||||
icon: 'mdi:image',
|
||||
};
|
||||
|
||||
components[`${svc.addr}_${metadata}`] = imageComponent;
|
||||
} else {
|
||||
// Other metadata as sensor entities
|
||||
const metadataComponent: SensorComponent = {
|
||||
unique_id: `${svc.addr}_${metadata}`,
|
||||
platform: 'sensor',
|
||||
name: `${metadata.charAt(0).toUpperCase() + metadata.slice(1)}`,
|
||||
value_template: `{{ value_json['${svc.addr}'].metadata.${metadata} | default('Unknown') }}`,
|
||||
icon: getMetadataIcon(metadata),
|
||||
};
|
||||
|
||||
components[`${svc.addr}_${metadata}`] = metadataComponent;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
components,
|
||||
commandHandlers,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get appropriate icon for playback mode
|
||||
*/
|
||||
function getPlaybackModeIcon(mode: string): string {
|
||||
const iconMap: Record<string, string> = {
|
||||
repeat: 'mdi:repeat',
|
||||
repeat_one: 'mdi:repeat-once',
|
||||
shuffle: 'mdi:shuffle',
|
||||
crossfade: 'mdi:shuffle-variant',
|
||||
};
|
||||
return iconMap[mode] || 'mdi:cog';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get appropriate icon for metadata type
|
||||
*/
|
||||
function getMetadataIcon(metadata: string): string {
|
||||
const iconMap: Record<string, string> = {
|
||||
album: 'mdi:album',
|
||||
track: 'mdi:music-note',
|
||||
artist: 'mdi:account-music',
|
||||
image_url: 'mdi:image',
|
||||
};
|
||||
return iconMap[metadata] || 'mdi:information';
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user