import jquery = require('jquery'); import ko = require('knockout'); import { Log } from 'log'; var instance: Stairs = null; export class StairsStaticMode { public Brightness = ko.observable(0); public Ease = ko.observable(false); public read(data: any): void { this.Brightness(data.brightness); } } export interface IStairsCustomBrightness { value: KnockoutObservable; } export class StairsCustomMode { public Brightness = ko.observableArray([]); public init(stepCount: number): void { // Each item is an object containing an observable; we can't // add the observable directly otherwise the foreach template // will unwrap it before we can attach it to the range input. var values: Array = []; for (var index = 0; index < stepCount; index++) values.push({ value: ko.observable(0) }); this.Brightness(values); } public read(data: any): void { var brightness = data.brightness.map((item: number): IStairsCustomBrightness => { return { value: ko.observable(item) }; }); brightness.reverse(); this.Brightness(brightness); } } export class StairsAlternateMode { public Interval = ko.observable(500); public Brightness = ko.observable(0); public read(data: any): void { this.Interval(data.interval); this.Brightness(data.brightness); } } export enum StairsMode { Unknown = 0, Static = 1, Custom = 2, Alternate = 3 } export class StairsModeParameters { public Static = new StairsStaticMode(); public Custom = new StairsCustomMode(); public Alternate = new StairsAlternateMode(); public Current = ko.observable(StairsMode.Unknown); public read(data: any): void { switch (this.Current()) { case StairsMode.Static: this.Static.read(data); break; case StairsMode.Custom: this.Custom.read(data); break; case StairsMode.Alternate: this.Alternate.read(data); break; } } } export class StairsRangeValue { public Start = ko.observable(0); public End = ko.observable(4095); } export class StairsRange { public UseScaling = ko.observable(false); public Values = ko.observableArray([]); public read(data: any): void { this.UseScaling(data.useScaling); var values = data.values.map((item: any): StairsRangeValue => { var value = new StairsRangeValue(); value.Start(item.start); value.End(item.end); return value; }); values.reverse(); this.Values(values); } } export class Stairs { public Loading = ko.observable(false); public SavingMode = ko.observable(false); public SavingSettings = ko.observable(false); public Saving = ko.pureComputed(() => { return this.SavingMode() || this.SavingSettings(); }); public Mode = new StairsModeParameters(); public Range = new StairsRange(); private pingTimer: number = null; private pingRequest: JQueryXHR = null; private updatingFromServer = true; private updateModeTimeout: number = null; private updateRangeTimeout: number = null; public static instance(): Stairs { if (instance == null) instance = new Stairs(); return instance; } constructor() { this.ping(); } public ping(): void { if (this.pingRequest !== null) { Log.verbose('Stairs.ping', 'Ping request already running, skipping'); return; } if (this.pingTimer !== null) { clearTimeout(this.pingTimer); this.pingTimer = null; } if (this.Loading() || this.Saving()) { this.pingTimer = setTimeout(() => this.ping(), 5000); return; } Log.verbose('Stairs.ping', 'Starting Ping request'); this.updatingFromServer = true; this.Loading(true); this.pingRequest = $.ajax({ url: '/ping', dataType: 'json', cache: false }); this.pingRequest.done((data: any) => { Log.verbose('Stairs.ping', data); this.Mode.Custom.init(data.stepCount); $.when(this.getMode(), this.getRange()) .done(() => this.pingComplete(true)) .fail(() => this.pingComplete(false)); }); this.pingRequest.fail(() => { Log.warning('Stairs.ping', 'Ping failed'); this.pingComplete(true); }); } private pingComplete(success: boolean) { this.pingRequest = null; if (success) //this.pingTimer = setTimeout(() => this.ping(), 5000); {} else this.pingTimer = setTimeout(() => this.ping(), 5000); if (success) { this.Loading(false); this.updatingFromServer = false; } } private getMode(): JQueryPromise { Log.verbose('Stairs.getMode', 'Requesting Mode'); var request = $.ajax({ url: '/getMode', dataType: 'json', cache: false }); request.done((response) => { Log.verbose('Stairs.getMode', response); this.Mode.Current(response.mode); this.Mode.read(response.data); }); return request; } private getRange(): JQueryPromise { Log.verbose('Stairs.getRange', 'Requesting Range configuration'); var request = $.ajax({ url: '/getRange', dataType: 'json', cache: false }); request.done((response) => { Log.verbose('Stairs.getRange', response); this.Range.read(response); }); return request; } private updateMode = ko.computed(() => { if (this.Loading() || this.SavingMode.peek()) return; var url = '/setMode/'; switch (this.Mode.Current()) { case StairsMode.Static: url += 'Static?brightness=' + encodeURIComponent(this.Mode.Static.Brightness().toString()); break; case StairsMode.Custom: var values = this.Mode.Custom.Brightness().map((item: IStairsCustomBrightness) => { return item.value(); }); values.reverse(); url += 'Custom?brightness=' + encodeURIComponent(values.join()); break; case StairsMode.Alternate: url += 'Alternate?interval=' + encodeURIComponent(this.Mode.Alternate.Interval().toString()) + '&brightness=' + encodeURIComponent(this.Mode.Alternate.Brightness().toString()); break; /* case 'Slide': url += '?interval=' + encodeURIComponent(this.slide.interval()) + '&brightness=' + encodeURIComponent(this.slide.brightness()) + '&direction=' + encodeURIComponent(this.slide.direction()) + '&fadeOutTime=' + encodeURIComponent(this.slide.fadeOutTime()); break; */ } // Exit after checking all the parameters, so the observers // are properly subscribed if (this.updatingFromServer) return; if (this.updateModeTimeout !== null) { clearTimeout(this.updateModeTimeout); this.updateModeTimeout = null; } this.updateModeTimeout = setTimeout(() => { Log.info("Stairs.updateMode", url); // TODO retry on failure this.SavingMode(true); $.ajax( { url: url, dataType: 'json', cache: false }) .always(() => { this.SavingMode(false); }); clearTimeout(this.updateModeTimeout); this.updateModeTimeout = null; }, 200); return true; }); private updateSettings = ko.computed(() => { if (this.Loading() || this.SavingSettings.peek()) return; var url = '/setRange?useScaling=' + this.Range.UseScaling().toString(); var start = this.Range.Values().map((item: StairsRangeValue): number => { return item.Start(); }); var end = this.Range.Values().map((item: StairsRangeValue): number => { return item.End(); }); start.reverse(); end.reverse(); url += '&start=' + encodeURIComponent(start.join()); url += '&end=' + encodeURIComponent(end.join()); // Exit after checking all the parameters, so the observers // are properly subscribed if (this.updatingFromServer) return; if (this.updateRangeTimeout !== null) { clearTimeout(this.updateRangeTimeout); this.updateRangeTimeout = null; } this.updateRangeTimeout = setTimeout(() => { Log.info("Stairs.updateSettings", url); // TODO retry on failure this.SavingSettings(true); $.ajax( { url: url, dataType: 'json', cache: false }) .always(() => { this.SavingSettings(false); }); clearTimeout(this.updateRangeTimeout); this.updateRangeTimeout = null; }, 200); return true; }); }