Skip to content

Commit

Permalink
Sonos Integration (GladysAssistant#1949)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pierre-Gilles authored Dec 1, 2023
1 parent aeddefc commit bfbf817
Show file tree
Hide file tree
Showing 40 changed files with 1,959 additions and 2 deletions.
Binary file added front/src/assets/integrations/cover/sonos.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions front/src/components/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ import TuyaEditPage from '../routes/integration/all/tuya/edit-page';
import TuyaSetupPage from '../routes/integration/all/tuya/setup-page';
import TuyaDiscoverPage from '../routes/integration/all/tuya/discover-page';

// Sonos integration
import SonosDevicePage from '../routes/integration/all/sonos/device-page';
import SonosDiscoveryPage from '../routes/integration/all/sonos/discover-page';

// MELCloud integration
import MELCloudPage from '../routes/integration/all/melcloud/device-page';
import MELCloudEditPage from '../routes/integration/all/melcloud/edit-page';
Expand Down Expand Up @@ -275,6 +279,9 @@ const AppRouter = connect(
<TuyaDiscoverPage path="/dashboard/integration/device/tuya/discover" />
<TuyaSetupPage path="/dashboard/integration/device/tuya/setup" />

<SonosDevicePage path="/dashboard/integration/device/sonos" />
<SonosDiscoveryPage path="/dashboard/integration/device/sonos/discover" />

<MELCloudPage path="/dashboard/integration/device/melcloud" />
<MELCloudEditPage path="/dashboard/integration/device/melcloud/edit/:deviceSelector" />
<MELCloudDiscoverPage path="/dashboard/integration/device/melcloud/discover" />
Expand Down
66 changes: 66 additions & 0 deletions front/src/components/boxs/music/EditMusicBox.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Component } from 'preact';
import { Text } from 'preact-i18n';
import Select from 'react-select';
import { connect } from 'unistore/preact';

import BaseEditBox from '../baseEditBox';
import { DEVICE_FEATURE_CATEGORIES } from '../../../../../server/utils/constants';

class EditMusicBoxComponent extends Component {
updateDevice = option => {
this.props.updateBoxConfig(this.props.x, this.props.y, {
device: option ? option.value : null
});
};

getDevices = async () => {
try {
await this.setState({
error: false
});
const musicDevices = await this.props.httpClient.get('/api/v1/device', {
device_feature_category: DEVICE_FEATURE_CATEGORIES.MUSIC
});
const musicDevicesOptions = musicDevices.map(d => ({
label: d.name,
value: d.selector
}));
this.setState({
musicDevicesOptions
});
} catch (e) {
console.error(e);
this.setState({
error: true
});
}
};

componentDidMount() {
this.getDevices();
}

render(props, { musicDevicesOptions }) {
let optionSelected = null;
if (musicDevicesOptions && props.box.device) {
optionSelected = musicDevicesOptions.find(o => o.value === props.box.device);
}
return (
<BaseEditBox {...props} titleKey="dashboard.boxTitle.music">
<div class="form-group">
<label>
<Text id="dashboard.boxes.music.selectDeviceLabel" />
</label>
<Select
defaultValue={null}
value={optionSelected}
onChange={this.updateDevice}
options={musicDevicesOptions}
/>
</div>
</BaseEditBox>
);
}
}

export default connect('httpClient', {})(EditMusicBoxComponent);
160 changes: 160 additions & 0 deletions front/src/components/boxs/music/MusicBox.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { Component } from 'preact';
import { connect } from 'unistore/preact';

import {
WEBSOCKET_MESSAGE_TYPES,
DEVICE_FEATURE_TYPES,
MUSIC_PLAYBACK_STATE
} from '../../../../../server/utils/constants';

class MusicComponent extends Component {
state = {
isPlaying: null
};
getDevice = async () => {
try {
await this.setState({
error: false
});
const musicDevice = await this.props.httpClient.get(`/api/v1/device/${this.props.box.device}`, {});
const playFeature = musicDevice.features.find(f => f.type === DEVICE_FEATURE_TYPES.MUSIC.PLAY);
const pauseFeature = musicDevice.features.find(f => f.type === DEVICE_FEATURE_TYPES.MUSIC.PAUSE);
const previousFeature = musicDevice.features.find(f => f.type === DEVICE_FEATURE_TYPES.MUSIC.PREVIOUS);
const nextFeature = musicDevice.features.find(f => f.type === DEVICE_FEATURE_TYPES.MUSIC.NEXT);
const volumeFeature = musicDevice.features.find(f => f.type === DEVICE_FEATURE_TYPES.MUSIC.VOLUME);
const playBackStateFeature = musicDevice.features.find(f => f.type === DEVICE_FEATURE_TYPES.MUSIC.PLAYBACK_STATE);
const isPlaying = playBackStateFeature.last_value === MUSIC_PLAYBACK_STATE.PLAYING;
this.setState({
musicDevice,
playFeature,
pauseFeature,
previousFeature,
nextFeature,
volumeFeature,
playBackStateFeature,
isPlaying
});
} catch (e) {
console.error(e);
this.setState({
error: true
});
}
};

setValueDevice = async (deviceFeature, value) => {
try {
await this.setState({ error: false });
await this.props.httpClient.post(`/api/v1/device_feature/${deviceFeature.selector}/value`, {
value
});
} catch (e) {
console.error(e);
this.setState({ error: true });
}
};

play = async () => {
await this.setState({ isPlaying: true });
await this.setValueDevice(this.state.playFeature, 1);
};
pause = async () => {
await this.setState({ isPlaying: false });
await this.setValueDevice(this.state.pauseFeature, 1);
};
next = async () => {
await this.setValueDevice(this.state.nextFeature, 1);
};
previous = async () => {
await this.setValueDevice(this.state.previousFeature, 1);
};
changeVolume = async e => {
const volume = parseInt(e.target.value, 10);
const newVolumeFeature = { ...this.state.volumeFeature, last_value: volume };
await this.setState({ volumeFeature: newVolumeFeature });
await this.setValueDevice(this.state.volumeFeature, volume, 10);
};

updateDeviceStateWebsocket = payload => {
if (payload.device_feature_selector === this.state.playBackStateFeature.selector) {
const isPlaying = payload.last_value === MUSIC_PLAYBACK_STATE.PLAYING;
this.setState({ isPlaying });
}
if (payload.device_feature_selector === this.state.volumeFeature.selector) {
const newVolumeFeature = { ...this.state.volumeFeature, last_value: payload.last_value };
this.setState({ volumeFeature: newVolumeFeature });
}
};

componentDidMount() {
this.getDevice();
this.props.session.dispatcher.addListener(
WEBSOCKET_MESSAGE_TYPES.DEVICE.NEW_STATE,
this.updateDeviceStateWebsocket
);
}

componentWillUnmount() {
this.props.session.dispatcher.removeListener(
WEBSOCKET_MESSAGE_TYPES.DEVICE.NEW_STATE,
this.updateDeviceStateWebsocket
);
}

render(props, { isPlaying, musicDevice, previousFeature, nextFeature, volumeFeature }) {
return (
<div class="card">
<div class="card-header">
<h3 class="card-title">{musicDevice && musicDevice.name}</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col">
{previousFeature && (
<button class="btn btn-block btn-secondary" onClick={this.previous}>
<i class="fe fe-skip-back" />
</button>
)}
</div>
<div class="col">
{!isPlaying && (
<button class="btn btn-block btn-secondary" onClick={this.play}>
<i class="fe fe-play" />
</button>
)}
{isPlaying && (
<button class="btn btn-block btn-secondary" onClick={this.pause}>
<i class="fe fe-pause" />
</button>
)}
</div>
<div class="col">
{nextFeature && (
<button class="btn btn-block btn-secondary" onClick={this.next}>
<i class="fe fe-skip-forward" />
</button>
)}
</div>
</div>
{volumeFeature && (
<div class="row mt-4">
<div class="col">
<input
type="range"
value={volumeFeature.last_value}
onChange={this.changeVolume}
class="form-control"
step="1"
min="0"
max="100"
/>
</div>
</div>
)}
</div>
</div>
);
}
}

export default connect('httpClient,session', {})(MusicComponent);
44 changes: 44 additions & 0 deletions front/src/config/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -3485,6 +3485,50 @@ const data = {
.subtract(3, 'hour')
.toDate()
}
],
'get /api/v1/service/sonos/device': [
{
id: 'c0e21639-4fe9-4d1c-ad65-33255d21bf0d',
name: 'Sonos Speaker',
external_id: 'sonos:uuid',
features: [
{
name: 'Sonos Play',
category: 'music',
type: 'play',
min: 1,
max: 1
}
]
}
],
'get /api/v1/service/sonos/discover': [
{
name: 'Sonos Speaker',
external_id: 'sonos:uuid',
features: [
{
name: 'Sonos Play',
category: 'music',
type: 'play',
min: 1,
max: 1
}
]
},
{
name: 'Sonos Speaker',
external_id: 'sonos:another_uuid',
features: [
{
name: 'Sonos Play',
category: 'music',
type: 'play',
min: 1,
max: 1
}
]
}
]
};

Expand Down
50 changes: 49 additions & 1 deletion front/src/config/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,8 @@
"chart": "Chart",
"ecowatt": "Ecowatt (France)",
"clock": "Clock",
"scene": "Scene"
"scene": "Scene",
"music": "Music"
},
"boxes": {
"column": "Column {{index}}",
Expand Down Expand Up @@ -387,6 +388,9 @@
"alarmStatusText": "Your house is ",
"alarmArming": "Your house is being armed...",
"cancelAlarmArming": "Cancel"
},
"music": {
"selectDeviceLabel": "Select device to control"
}
}
},
Expand Down Expand Up @@ -894,6 +898,41 @@
"conflictError": "Current device is already in Gladys."
}
},
"sonos": {
"title": "Sonos",
"description": "Control Sonos devices on your local network",
"deviceTab": "Devices",
"discoverTab": "Sonos Discovery",
"setupTab": "Setup",
"documentation": "Sonos Documentation",
"discoverDeviceDescr": "Automatically scan for Sonos devices",
"nameLabel": "Device Name",
"namePlaceholder": "Enter your device name",
"hostLabel": "IP Address",
"roomLabel": "Room",
"saveButton": "Save",
"alreadyCreatedButton": "Already Created",
"deleteButton": "Delete",
"device": {
"title": "Sonos Devices in Gladys",
"editButton": "Edit",
"noDeviceFound": "No Sonos devices found.",
"featuresLabel": "Features"
},
"discover": {
"title": "Devices detected on your local network",
"description": "Sonos devices are automatically discovered.",
"error": "Error discovering Sonos devices. Is your Sonos speaker powered on and accessible on the local network?",
"noDeviceFound": "No Sonos devices were discovered.",
"errorWhileScanning": "An error occurred during scanning.",
"scan": "Scan"
},
"error": {
"defaultError": "An error occurred while saving the device.",
"defaultDeletionError": "An error occurred while deleting the device.",
"conflictError": "The current device is already in Gladys."
}
},
"melcloud": {
"title": "MELCloud",
"description": "Control your MELCloud devices (works with the cloud)",
Expand Down Expand Up @@ -2697,6 +2736,15 @@
"shortCategoryName": "Surface",
"decimal": "Surface"
},
"music": {
"shortCategoryName": "Music",
"volume": "Music volume",
"play": "Music play button",
"pause": "Music pause button",
"previous": "Music previous button",
"next": "Music next button",
"playback_state": "Music playback state"
},
"unknown": {
"shortCategoryName": "Unknown",
"unknown": "Unknown"
Expand Down
Loading

0 comments on commit bfbf817

Please sign in to comment.