Created
April 30, 2025 14:07
-
-
Save FlyInk13/fc1f2dbda3aca809be2a535131fd47fe to your computer and use it in GitHub Desktop.
Локальное управление Яндекс Станцией
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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