Skip to content

Instantly share code, notes, and snippets.

@FlyInk13
Created April 30, 2025 14:07
Show Gist options
  • Save FlyInk13/fc1f2dbda3aca809be2a535131fd47fe to your computer and use it in GitHub Desktop.
Save FlyInk13/fc1f2dbda3aca809be2a535131fd47fe to your computer and use it in GitHub Desktop.
Локальное управление Яндекс Станцией
const WebSocket = require('ws');
const http = require('http');
// Sources:
// https://github.com/n0name45/node-red-contrib-yandex-station-management
// https://www.npmjs.com/package/ws
// How to run this:
// 1) Install nodejs
// 2) npm install ws
// 3) Open yandex music auth: https://oauth.yandex.ru/authorize?response_type=token&client_id=23cabbbdc6cd418abb4b39c32c41195d
// 4) copy access_token from url
// 5) export TOKEN="access_token"
// 6) export DEVICE_NAME='Станция Мини'
// 7) node index.js
const token = process.env.TOKEN;
const deviceName = process.env.DEVICE_NAME;
// Command examples:
// curl -d '{"command":"setVolume", "volume": 0.5}' -X POST http://localhost:3000
// curl -d '{"command":"sendText", "text": "погода"}' -X POST http://localhost:3000
// Available commands:
// https://flows.nodered.org/node/node-red-contrib-yandex-station-management
class Alice {
constructor({ id, name, ip, port, platform, token, server_private_key, server_certificate }) {
this.device = { id, name, ip, port, platform, token, server_private_key, server_certificate };
this.ws = null;
this.sentCommands = {};
this.state = null;
}
static async getDevice(deviceName) {
const { devices } = await Alice._getFromGlagol('/device_list');
console.info('Доступные устройства:')
devices.map(x => {
const ip = x.networkInfo.ip_addresses.find(x => /192/.test(x));
console.log(JSON.stringify([x.id, x.name, ip]));
});
const device = devices.find(x => x.name === deviceName);
if (!device) {
throw new Error('Device "' + deviceName + '" not found');
}
const { token: deviceToken } = await Alice._getFromGlagol('/token?device_id=' + device.id + '&platform=' + device.platform);
if (!deviceToken) {
throw new Error('deviceToken not found');
}
const deviceIp = device.networkInfo.ip_addresses.find(x => /192/.test(x))
if (!deviceIp) {
throw new Error('deviceIp not found');
}
return new Alice({
id: device.id,
name: device.name,
ip: deviceIp,
port: device.networkInfo.external_port,
platform: device.platform,
token: deviceToken,
server_private_key: device.glagol.security.server_private_key,
server_certificate: device.glagol.security.server_certificate,
});
}
static async _getFromGlagol(url) {
const res = await fetch('https://quasar.yandex.net/glagol' + url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': 'OAuth ' + token,
}
});
const data = await res.json();
return data;
}
connect() {
this.ws = new WebSocket(`wss://${this.device.ip}:${this.device.port}`, {
rejectUnauthorized: false,
key: this.device.server_private_key,
cert: this.device.server_certificate,
});
this.ws.addEventListener('message', (data) => this._onMessage(JSON.parse(data.data)));
return this.ws;
}
_onMessage(message) {
if (message && message.state && JSON.stringify(message.state) !== JSON.stringify(this.state)) {
this.state = message.state;
}
if (!message.requestSentTime) {
return;
}
const key = message.requestSentTime.toString();
if (!this.sentCommands[key]) {
console.warn('Результат чужой команды: ', message.status, message.sentTime, message.requestSentTime);
return;
}
console.log(message.status, this.sentCommands[key]);
delete this.sentCommands[key];
}
async sendMessage(payload) {
if (!this.ws || this.ws.readyState !== 1) {
throw new Error('Empty connection');
}
const sentTime = Date.now();
this.sentCommands[sentTime.toString()] = payload;
return this.ws.send(JSON.stringify({
"conversationToken": this.device.token,
"id": this.device.id,
"payload": payload,
"sentTime": sentTime
}));
}
}
let _alice = null;
const server = http.createServer((req, res) => {
if (req.method !== 'POST') {
res.writeHead(405, { 'Content-Type': 'text/plain' });
res.end('Method Not Allowed\n');
return;
}
if (!_alice) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Not Ready\n');
return;
}
let body = '';
req.on('data', chunk => body += chunk.toString());
req.on('end', () => {
Promise.resolve().then(async () => {
const parsedData = JSON.parse(body);
await _alice.sendMessage(parsedData);
return _alice.state;
}).then((state) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(JSON.stringify(state, 2, null));
}).catch(e => {
console.error('error:', e);
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Данные успешно получены (текст)!\n');
})
});
});
Alice.getDevice(deviceName).then(async (alice) => {
const ws = alice.connect();
ws.addEventListener('error', (error) => console.error('Error:', error));
ws.addEventListener('close', (e) => console.log('Disconnected', e.reason));
ws.addEventListener('open', () => {
_alice = alice;
console.log('Connected to Yandex Station');
server.listen(3000, () => {
console.log(`Сервер запущен и слушает порт 3000`);
});
});
}).catch(e => {
console.error(e)
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment