//# sourceURL=jobs/VeraController.js
/**
 * $Id: Controller.js 279 2017-10-28 22:33:19Z patrick $
 */
/* globals define,require,$,Promise,VeraController */

import Controller from '../Controller.js';
import Group from '../entities/Group.js';
import * as util from '../util.js';

var MAX_FAIL = 3;
var ERROR_DELAY = 5000;

var VeraController = function( source ) {
	Controller.call( this, source );

	this.reloads = 0;
	this.reloadBase = new Date().getTime();
	this.reqTime = 0;
	this.lastUpdate = 0;
	this.loadtime = 0;
	this.dataversion = 0;
	this.failCount = 0;
	this.deviceMap = {};

	/* Unit starting with ! forces proxy use */
	this.useProxy = false;
	if ( 0 === this.unit.indexOf('!') ) {
		this.useProxy = true;
		this.unit = this.unit.substr(1);
	}
};

VeraController.prototype = Object.create(Controller.prototype);
VeraController.prototype.constructor = VeraController;

/**
 * Make a request to Vera system, returns a Promise.
 */
VeraController.prototype.verarequest = function(p, opts) {
	var self = this;
	this.log.debug(8, "%1: sending Vera request: ", this, util.dump(p));
	/* This approach requires CORS. Execute the following on your Vera (firmware 7.30 or higher only):
	   luup.attr_set( "AllowCORS", "1", 0 )
	   luup.reload();
	*/
	var uri = 'http://' + self.unit + '/port_3480/data_request';
	if ( self.useProxy ) {
		p.url = uri;
		uri = SlapDash.config.baseurl + '/api/proxy';
	}
	p.output_format = 'json';

	var nopt = Object.assign( {}, opts || {} );
	nopt.url = uri;
	nopt.data = p;
	nopt.dataType = 'json';
	nopt.timeout = nopt.timeout || 15000;
	return new Promise( function( resolve, reject ) {
		$.ajax( nopt ).done( function( data ) {
			resolve( data );
		}).fail(function(st) {
			// console.log(st);
			self.log.err(st.statusText);
			reject(new Error(st.statusText));
		});
	});
};

VeraController.prototype.getRoomId = function( data, room ) {
	return this.getGroupId( data, room );
};

VeraController.prototype.addToGroup = function( e, groupId ) {
	var g = this.getEntity( Group, groupId );
	g.addMember( e );
};

VeraController.prototype.updateDeviceEntity = function( className, id, data ) {
	var self = this;
	require( [ "entities/" + className ], function( classdef ) {
		var e = self.getEntity( classdef, id ); /* does not fail */
		e.deferNotifies( true );
		try {
			var skip = { "/State": true }; /* states we'll ignore later */
			if ( data.name ) {
				e.setName( data.name );
			}
			var states = {};
			var nstates = data.states ? data.states.length : 0;
			for ( var is=0; is<nstates; ++is ) {
				var key = data.states[is].service + "/" + data.states[is].variable;
				var val = data.states[is].value;
				if ( val && val.match( /^[0-9]+$/ ) ) {
					val = parseInt( val );
				}
				states[key] = val;
			}
			function S( key ) {
				if ( undefined !== states[key] ) e.setState( states[key] );
			}
			function L( key ) {
				if ( undefined !== states[key] ) e.setAttribute( "level", parseFloat( states[key] ) || 0 );
			}
			if ( classdef.name == "Switch" ) {
				S("urn:upnp-org:serviceId:SwitchPower1/Status");
			} else if ( classdef.name == "Light" ) {
				S( "urn:upnp-org:serviceId:SwitchPower1/Status" );
				if ( undefined !== states["urn:upnp-org:serviceId:Dimming1/LoadLevelStatus"] ) {
					e.setAttribute( "brightness", states["urn:upnp-org:serviceId:Dimming1/LoadLevelStatus"] );
				}
			} else if ( classdef.name == "BinarySensor" ) {
				S( "urn:micasaverde-com:serviceId:SecuritySensor1/Tripped" );
				skip["/ArmedTripped"] = true; /* ??? */
			} else if ( classdef.name == "Thermostat" ) {
				S( "urn:upnp-org:serviceId:HVAC_UserOperatingMode1/ModeStatus" );
			} else if ( classdef.name == "Camera" ) {
				/* ??? */
			} else if ( classdef.name == "Lock" ) {
				S( "urn:micasaverde-com:serviceId:DoorLock1/Status" );
			} else if ( classdef.name == "Cover" ) {
				S( "urn:upnp-org:serviceId:SwitchPower1/Status" );
			} else if ( classdef.name == "MediaPlayer" ) {
				S( "urn:upnp-org:serviceId:AVTransport/TransportState" );
			} else if ( classdef.name == "ValueSensor" ) {
				var cat = parseInt( data.category_num || -1 );
				if ( cat < 0 ) cat = e.getAttribute( "category", -1 ); /* category_num does not appear on updates */
				switch ( cat ) {
					case 12: /* Generic sensor */
						L( "urn:micasaverde-com:serviceId:GenericSensor1/CurrentLevel" );
						break;
					case 16: /* Humidity sensor */
						L( "urn:micasaverde-com:serviceId:HumiditySensor1/CurrentLevel" );
						break;
					case 17: /* Temp sensor */
						L( "urn:upnp-org:serviceId:TemperatureSensor1/CurrentTemperature" );
						break;
					case 18: /* lux sensor */
					case 28: /* UV sensor */
						L( "urn:micasaverde-com:serviceId:LightSensor1/CurrentLevel" );
						break;
					default:
						self.log.debug(2, "Unrecognized category %1 for %2 (%3), setting default state false",
							data.category_num, e, classdef.name);
						e.setState( false );
				}
			} else {
				self.log.debug(2, "%1 setting default state(false) for %2", self, e);
				e.setState( false );
			}
			for ( var key in states ) {
				if ( states.hasOwnProperty(key) ) {
					/* Canonicalize the service ID and variable name to a safe string */
					var n = key.toLowerCase().replace(/ +/g, "_")
						.replace( /(urn|serviceid)\:/g, "" )
						.replace( /[^a-z_]/ig, "_" );
					var q = key.replace( /^[^\/]+/, "" ); // remove service, test "/Variable"
					if ( ! ( skip[key] || skip[n] || skip[q] ) ) {
						e.setAttribute( n, states[key] );
					}
				}
			}
			if ( data.room ) {
				self.addToGroup( e, data.room );
			}
			if ( undefined !== data.category_num ) {
				e.setAttribute( "category", parseInt( data.category_num ) );
			}
			self.deviceMap[id] = e;
			e.setAttribute("lastupdate", self.reqTime);
		} catch ( ex ) {
			console.log(ex);
		}
		e.deferNotifies( false );
	});
};

VeraController.prototype.updateSimpleEntity = function( className, id, data ) {
	var self = this;
	require( [ "entities/" + className ], function( classdef ) {
		var e = self.getEntity( classdef, id ); /* does not fail */
		if ( data.name ) e.setName( data.name );
		for ( var key in data ) {
			if ( data.hasOwnProperty(key) ) {
				var n = key.toLowerCase().replace(/ /g, "_");
				e.setAttribute( n, data[key] );
			}
		}
		if ( data.room ) {
			self.addToGroup( e, data.room );
		}
		e.deferNotifies(false);
	});
};

VeraController.prototype.update = function() {
	var self = this;
	self.reqTime = new Date().getTime();

//console.log(self.toString() + " sending request with loadtime=" + self.loadtime + " dataversion=" + self.dataversion);
	var reqData = { id: "status" };
	if ( self.dataversion > 0 && self.loadtime > 0 ) {
		self.log.debug(5, "Requesting delta update")
		reqData.DataVersion = self.dataversion;
		reqData.LoadTime = self.loadtime;
		reqData.Timeout = 15; /* yes, seconds */
		reqData.MinimumDelay = 200;
	} else {
		reqData.id = "user_data";
	}
	self.verarequest( reqData, { xhrFields: { withCredentials: false }, timeout: reqData.Timeout ? (reqData.Timeout * 1200) : 15000 } )
		.then( function( data ) {
//console.log(self.toString() + " response with loadtime=" + data.LoadTime + " dataversion=" + data.DataVersion + " full=" + data.full);
			/* Sanity-check response */
			if ( undefined == data.LoadTime || undefined == data.DataVersion ) {
				self.log.warn("%1: unparseable response from server", this);
				if ( ++self.failCount >= MAX_FAIL ) {
					self.fail();
				}
				self.startDelay( ERROR_DELAY );
				return;
			}
			if ( self.loadtime > 0 && self.loadtime !== data.LoadTime ) {
				++self.reloads;
			}

			// Device data
			if ( data.devices ) {
				// console.log("Update contains "+data.devices.length)
				var ndev = data.devices.length;
				for (var ix=0; ix<ndev; ++ix) {
					var d = data.devices[ix];
					var deviceid = String(d.id);
					// console.log("   > "+deviceid+" "+String(d.name));
					var className;
					if ( self.deviceMap[deviceid] ) {
						className = self.deviceMap[deviceid].getTypeName();
					} else {
						className = {
							/* 1:"Interface", */
							2:"Light",
							3:"Switch",
							4:"BinarySensor",
							5:"Thermostat",
							/* 6:"Camera", */
							7:"Lock",
							/* 8:"Cover", */
							11:"Switch", /* Generic I/O */
							12:"ValueSensor", /* Generic sensor */
							/* 13:"SceneCtrl", /* Scene controller */
							15:"MediaPlayer",
							16:"ValueSensor", /* Humidity */
							17:"ValueSensor", /* Temp */
							18:"ValueSensor", /* Light (lux) */
							24:"Switch",      /* Siren */
							28:"ValueSensor", /* UV */
							33:"ValueSensor", /* Flow */
							34:"ValueSensor"  /* Voltage */
						}[d.category_num||0];
					}
					if ( className ) {
						try {
							self.updateDeviceEntity( className, deviceid, d );
						} catch( e ) {
							console.log("Failed to update device #"+deviceid);
							console.log(e);
						}
					} else {
						self.log.debug(9, "%1 skipping vera device %2 category %3, no Entity defined.", self, deviceid, d.category_num);
					}
				}
			}
			if ( data.scenes ) {
				var nscenes = data.scenes.length;
				for (var ix=0; ix<nscenes; ++ix) {
					var d = data.scenes[ix];
					var scid = String(d.id);
					self.log.debug(9, "%1: Updating scene %2", self, scid);
					d.state = parseInt( d.modeStatus ) || 0;
					self.updateSimpleEntity( "Script", scid, d );
				}
			}
			if ( data.rooms ) {
				var nrooms = data.rooms.length;
				for (var ix=0; ix<nrooms; ++ix) {
					var d = data.rooms[ix];
					var rid = String(d.id);
					self.log.debug(9, "%1: Updating room %2", self, rid);
					d.state = 0;
					self.updateSimpleEntity( "Group", rid, d );
				}
			}

			self.failCount = 0;
			self.lastUpdate = self.reqTime;
			self.loadtime = data.LoadTime;
			self.dataversion = data.DataVersion;

			/* Update the system entity */
			self.system.deferNotifies( true );
			// self.system.setAttribute("vera", data);
			self.system.setAttribute("state", true);
			self.system.setAttribute("error", false);
			self.system.setAttribute("message", data.comment || "");
			self.system.setAttribute("lastupdate", self.reqTime);
			self.system.setAttribute("reloads", self.reloads);
			self.system.setAttribute("reloadbase", self.reloadBase);
			self.system.setAttribute("uptime", Math.floor(new Date().getTime()/1000) - self.loadtime);
			self.system.deferNotifies( false );

			/* Notify for the controller Entity. Alternative to System entity. */
			self.notifyObservers( self );

			/* Schedule next request. */
			self.startDelay( 250 ); /* quick turn */
		}).catch( function( err ) {
			self.dataversion = 0; /* force full load next */
			if ( ++self.failCount >= MAX_FAIL ) {
				self.fail();
			}
			self.startDelay( ERROR_DELAY );
		});
};

VeraController.prototype.fail = function() {
	this.system.setAttributes({ state: false, error: true, message:"Lost communication" });
};

VeraController.prototype.start = function() {
	var self = this;
	return new Promise( function( resolve, reject ) {
		self.loadtime = self.dataversion = 0;
		self.run();
		resolve( self );
	});
};

VeraController.prototype.run = function() {
	this.update();
};

VeraController.prototype.deviceaction = function(device, p, f)  {
	var self = this;
	p.id = "action";
	p.DeviceNum = device;
	this.verarequest( p, f ).catch(function(e) { self.log.err("Device action failed: %1", e); } );
};

VeraController.prototype.action_setState = function( entity, state ) {
	var typ = entity.getTypeName();
	if ( "Lock" === typ ) {
		this.deviceaction( entity.getId(), {
			serviceId: "urn:micasaverde-com:serviceId:DoorLock1",
			action: "SetTarget",
			newTargetValue: state == 'on' ? 1 : 0
		});
	} else {
		/* OK for Switch, Light */
		this.deviceaction( entity.getId(), {
			serviceId: "urn:upnp-org:serviceId:SwitchPower1",
			action: "SetTarget",
			newTargetValue: state == 'on' ? 1 : 0
		});
	}
};

VeraController.prototype.action_setLevel = function( entity, level ) {
	this.deviceaction( entity.getId(), {
			serviceId: "urn:upnp-org:serviceId:Dimming1",
			action: "SetLoadLevelTarget",
			newLoadlevelTarget: level
	});
};

VeraController.prototype.action_runScene = function( entity ) {
	// http://ip_address:3480/data_request?id=action&serviceId=urn:micasaverde-com:serviceId:HomeAutomationGateway1&action=RunScene&SceneNum=<SceneNum>
	this.verarequest({
			id: "action",
			serviceId: "urn:micasaverde-com:serviceId:HomeAutomationGateway1",
			action: "RunScene",
			SceneNum: entity.getId()
	});
};

VeraController.prototype.action_mediaplayer_prev = function( entity ) {
	this.deviceaction( entity.getId(), {
			serviceId: "urn:micasaverde-com:serviceId:MediaNavigation1",
			action: "SkipUp"
	});
};

VeraController.prototype.action_mediaplayer_next = function( entity ) {
	this.deviceaction( entity.getId(), {
			serviceId: "urn:micasaverde-com:serviceId:MediaNavigation1",
			action: "SkipDown"
	});
};

VeraController.prototype.action_mediaplayer_play = function( entity ) {
	this.deviceaction( entity.getId(), {
			serviceId: "urn:micasaverde-com:serviceId:MediaNavigation1",
			action: "Play"
	});
};

VeraController.prototype.action_mediaplayer_pause = function( entity ) {
	this.deviceaction( entity.getId(), {
			serviceId: "urn:micasaverde-com:serviceId:MediaNavigation1",
			action: "Pause"
	});
};

VeraController.prototype.action_mediaplayer_stop = function( entity ) {
	this.deviceaction( entity.getId(), {
			serviceId: "urn:micasaverde-com:serviceId:MediaNavigation1",
			action: "Stop"
	});
};

export default VeraController;
