Add support for 'media_player' service

This commit is contained in:
Adrian Jagielak 2025-07-25 15:40:28 +02:00
parent c84c2744e0
commit 1807b5157b
No known key found for this signature in database
GPG Key ID: 0818CF7AF6C62BFB
8 changed files with 363 additions and 5 deletions

View File

@ -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) | ✅ |

View File

@ -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).

View File

@ -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) | ✅ |

View File

@ -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"

View File

@ -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,
};

View File

@ -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
}
]

View File

@ -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"
}
]
}
]

View 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';
}