Compare commits

..

7 Commits

Author SHA1 Message Date
Koushik Dutta
47f603877b prepublish 2023-02-24 16:01:29 -08:00
Koushik Dutta
9adc803206 prepublish 2023-02-24 16:00:47 -08:00
Koushik Dutta
031e290017 google-device-access: implement new api 2023-02-24 11:50:19 -08:00
Koushik Dutta
a9ab2d0110 google-device-access: use new state api 2023-02-24 11:42:52 -08:00
Koushik Dutta
9592b6087b Merge branch 'main' of github.com:koush/scrypted 2023-02-24 11:30:21 -08:00
Koushik Dutta
5c34213b5d google-device-access: fixup project 2023-02-24 11:30:15 -08:00
Nick Berardi
4435fe1e0a alexa: ensure we are talking to the correct API endpoint (#580)
* alexa plugin: ensure we are talking to the right endpoint region for a customer

* added the api endpoint as a visible setting
2023-02-24 09:41:11 -08:00
6 changed files with 127 additions and 19 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/alexa",
"version": "0.0.20",
"version": "0.1.0",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",
@@ -35,7 +35,7 @@
"dependencies": {
"@types/node": "^16.6.1",
"alexa-smarthome-ts": "^0.0.1",
"axios": "^0.24.0",
"axios": "^1.3.4",
"uuid": "^9.0.0"
},
"devDependencies": {

View File

@@ -1,5 +1,5 @@
import axios from 'axios';
import sdk, { HttpRequest, HttpRequestHandler, HttpResponse, MixinProvider, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface } from '@scrypted/sdk';
import sdk, { HttpRequest, HttpRequestHandler, HttpResponse, MixinProvider, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, Setting, SettingValue, Settings } from '@scrypted/sdk';
import { StorageSettings } from '@scrypted/sdk/storage-settings';
import { AutoenableMixinProvider } from '@scrypted/common/src/autoenable-mixin-provider';
import { isSupported } from './types';
@@ -12,16 +12,22 @@ const { systemManager, deviceManager } = sdk;
const client_id = "amzn1.application-oa2-client.3283807e04d8408eb44a698c10f9dd13";
const client_secret = "bed445e2b26730acd818b90e175b275f6b67b18ff8645e571c5b3e311fa75ee9";
class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler, MixinProvider {
class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler, MixinProvider, Settings {
storageSettings = new StorageSettings(this, {
tokenInfo: {
hide: true,
json: true,
json: true
},
syncedDevices: {
multiple: true,
hide: true,
hide: true
},
apiEndpoint: {
title: 'Alexa Endpoint',
description: 'This is the endpoint Alexa will use to send events to. This is set after you login.',
type: 'string',
readonly: true
}
});
handlers = new Map<string, AlexaHandler>();
@@ -85,6 +91,13 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
});
}
getSettings(): Promise<Setting[]> {
return this.storageSettings.getSettings();
}
putSetting(key: string, value: SettingValue): Promise<void> {
return this.storageSettings.putSetting(key, value);
}
async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any; }): Promise<any> {
return mixinDevice;
}
@@ -99,11 +112,41 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
deviceManager.requestRestart();
}
readonly endpoints: string[] = [
'api.amazonalexa.com',
'api.eu.amazonalexa.com',
'api.fe.amazonalexa.com'
];
async getAlexaEndpoint() : Promise<string> {
if (this.storageSettings.values.apiEndpoint)
return this.storageSettings.values.apiEndpoint;
try {
const accessToken = await this.getAccessToken();
const response = await axios.get(`https://${this.endpoints[0]}/v1/alexaApiEndpoint`, {
headers: {
'Authorization': 'Bearer ' + accessToken,
}
});
const endpoint: string = response.data.endpoints[0];
this.storageSettings.values.apiEndpoint = endpoint;
return endpoint;
} catch (err) {
this.console.error(err);
// default to NA/RoW endpoint if we can't get the endpoint.
return this.endpoints[0];
}
}
async postEvent(data: any) {
const accessToken = await this.getAccessToken();
const endpoint = await this.getAlexaEndpoint();
const self = this;
return axios.post('https://api.amazonalexa.com/v3/events', data, {
return axios.post(`https://${endpoint}/v3/events`, data, {
headers: {
'Authorization': 'Bearer ' + accessToken,
}
@@ -267,6 +310,7 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
const json = JSON.parse(request.body);
const { grant } = json.directive.payload;
this.storageSettings.values.tokenInfo = grant;
this.storageSettings.values.apiEndpoint = undefined;
this.accessToken = undefined;
const self = this;
@@ -274,6 +318,7 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
self.console.error(`Failed to handle the AcceptGrant directive because ${reason}`);
this.storageSettings.values.tokenInfo = undefined;
this.storageSettings.values.apiEndpoint = undefined;
this.accessToken = undefined;
response.send(JSON.stringify({
@@ -311,6 +356,7 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
this.console.error(`AcceptGrant.Response failed because ${error}`);
this.storageSettings.values.tokenInfo = undefined;
this.storageSettings.values.apiEndpoint = undefined;
this.accessToken = undefined;
throw error;
}
@@ -455,7 +501,7 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
catch (e) {
this.console.error(`request failed due to invalid authorization`, e);
response.send(e.message, {
code: 500,
code: 500
});
return;
}

View File

@@ -113,6 +113,7 @@ export class AlexaSignalingSession implements RTCSignalingSession {
// this could be a low resolution screen, no way of knowing, so never send a
// 1080p+ stream.
screen: {
devicePixelRatio: 1, // TODO: get this from the device
width: 1280,
height: 720,
}

View File

@@ -1 +0,0 @@
../../../common/fs/unavailable.jpg

View File

@@ -1,14 +1,13 @@
import sdk, { DeviceManifest, DeviceProvider, HttpRequest, HttpRequestHandler, HttpResponse, HumiditySensor, MediaObject, MotionSensor, OauthClient, Refresh, ScryptedDeviceType, ScryptedInterface, Setting, Settings, TemperatureSetting, TemperatureUnit, Thermometer, ThermostatMode, VideoCamera, MediaStreamOptions, BinarySensor, DeviceInformation, RTCAVSignalingSetup, Camera, PictureOptions, ObjectsDetected, ObjectDetector, ObjectDetectionTypes, FFmpegInput, RequestMediaStreamOptions, Readme, RTCSignalingChannel, RTCSessionControl, RTCSignalingSession, ResponseMediaStreamOptions, RTCSignalingOptions, RTCSignalingSendIceCandidate, ScryptedMimeTypes, MediaStreamUrl } from '@scrypted/sdk';
import { ScryptedDeviceBase } from '@scrypted/sdk';
import qs from 'query-string';
import ClientOAuth2 from 'client-oauth2';
import { URL } from 'url';
import axios from 'axios';
import throttle from 'lodash/throttle';
import { connectRTCSignalingClients } from '@scrypted/common/src/rtc-signaling';
import { sleep } from '@scrypted/common/src/sleep';
import fs from 'fs';
import sdk, { BinarySensor, Camera, DeviceInformation, DeviceManifest, DeviceProvider, FFmpegInput, HttpRequest, HttpRequestHandler, HttpResponse, HumiditySensor, MediaObject, MediaStreamUrl, MotionSensor, OauthClient, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, PictureOptions, Readme, Refresh, RequestMediaStreamOptions, ResponseMediaStreamOptions, RTCAVSignalingSetup, RTCSessionControl, RTCSignalingChannel, RTCSignalingSendIceCandidate, RTCSignalingSession, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, TemperatureCommand, TemperatureSetting, TemperatureUnit, Thermometer, ThermostatMode, VideoCamera } from '@scrypted/sdk';
import axios from 'axios';
import ClientOAuth2 from 'client-oauth2';
import { randomBytes } from 'crypto';
import fs from 'fs';
import throttle from 'lodash/throttle';
import qs from 'query-string';
import { URL } from 'url';
const { deviceManager, mediaManager, endpointManager, systemManager } = sdk;
@@ -101,6 +100,10 @@ class NestRTCSessionControl implements RTCSessionControl {
constructor(public camera: NestCamera, public options: { streamExtensionToken: string, mediaSessionId: string }) {
}
async setPlayback(options: { audio: boolean; video: boolean; }): Promise<void> {
}
async getRefreshAt(): Promise<number> {
return this.refreshAt;
}
@@ -372,6 +375,52 @@ class NestThermostat extends ScryptedDeviceBase implements HumiditySensor, Therm
this.reload();
}
async setTemperature(command: TemperatureCommand): Promise<void> {
// set this in case round trip is slow.
let { mode, setpoint } = command;
if (mode) {
const nestMode = toNestMode(mode);
this.device.traits['sdm.devices.traits.ThermostatMode'].mode = nestMode;
this.executeCommandSetMode = {
command: 'sdm.devices.commands.ThermostatMode.SetMode',
params: {
mode: nestMode,
},
}
}
if (command.setpoint) {
mode ||= fromNestMode(this.device.traits['sdm.devices.traits.ThermostatMode'].mode);
this.executeCommandSetCelsius = {
command: setpointReverseMap.get(mode),
params: {
},
};
if (typeof command.setpoint === 'number') {
if (mode === ThermostatMode.Heat) {
this.executeCommandSetCelsius.params.heatCelsius = command.setpoint;
}
else if (mode === ThermostatMode.Cool) {
this.executeCommandSetCelsius.params.coolCelsius = command.setpoint;
}
else {
this.executeCommandSetCelsius.params.coolCelsius = command.setpoint;
this.executeCommandSetCelsius.params.heatCelsius = command.setpoint;
}
}
else {
this.executeCommandSetCelsius.params.heatCelsius = command[0];
this.executeCommandSetCelsius.params.coolCelsius = command[1];
}
}
await this.executeThrottle();
await this.refresh(null, true);
}
async setTemperatureUnit(temperatureUnit: TemperatureUnit): Promise<void> {
// not supported by API. throw?
}
@@ -398,26 +447,37 @@ class NestThermostat extends ScryptedDeviceBase implements HumiditySensor, Therm
const heat = device.traits?.['sdm.devices.traits.ThermostatTemperatureSetpoint']?.heatCelsius;
const cool = device.traits?.['sdm.devices.traits.ThermostatTemperatureSetpoint']?.coolCelsius;
let setpoint: number | [number, number];
if (this.thermostatMode === ThermostatMode.Heat) {
this.thermostatSetpoint = heat;
this.thermostatSetpointHigh = undefined;
this.thermostatSetpointLow = undefined;
setpoint = heat;
}
else if (this.thermostatMode === ThermostatMode.Cool) {
this.thermostatSetpoint = cool;
this.thermostatSetpointHigh = undefined;
this.thermostatSetpointLow = undefined;
setpoint = cool;
}
else if (this.thermostatMode === ThermostatMode.HeatCool) {
this.thermostatSetpoint = undefined;
this.thermostatSetpointHigh = heat;
this.thermostatSetpointLow = cool;
setpoint = [heat, cool];
}
else {
this.thermostatSetpoint = undefined;
this.thermostatSetpointHigh = undefined;
this.thermostatSetpointLow = undefined;
}
this.temperatureSetting = {
activeMode: this.thermostatActiveMode,
mode: this.thermostatMode,
setpoint,
availableModes: modes,
}
}
async refresh(refreshInterface: string, userInitiated: boolean): Promise<void> {
@@ -560,6 +620,8 @@ export class GoogleSmartDeviceAccess extends ScryptedDeviceBase implements Oauth
}
})();
}
async releaseDevice(id: string, nativeId: string): Promise<void> {
}
async onRequest(request: HttpRequest, response: HttpResponse): Promise<void> {
const payload = JSON.parse(Buffer.from(JSON.parse(request.body).message.data, 'base64').toString());

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/server",
"version": "0.6.20",
"version": "0.6.22",
"description": "",
"dependencies": {
"@ffmpeg-installer/ffmpeg": "^1.1.0",
@@ -69,7 +69,7 @@
"build": "tsc --outDir dist",
"postbuild": "node test/check-build-output.js",
"prepublishOnly": "npm version patch && git add package.json && npm run build && git commit -m prepublish",
"postpublish": "git push origin v$npm_package_version",
"postpublish": "git tag v$npm_package_version && git push origin v$npm_package_version",
"docker": "scripts/github-workflow-publish-docker.sh"
},
"author": "",