Skip to content

Scripting

adquio permite automatizar tareas mediante scripting. Puede reaccionar a cualquier evento y programar el sistema para un comportamiento a medida utilizando esta característica.

Introducción al funcionamiento de los scripts

Cada script es arrancado en el momento en el que se activa o cuando el aparato arranca.

El manejo de eventos o acciones temporizadas se consiguen instanciando manejadores, que son pequeñas funciones que serán ejecutadas como respuesta a un evento o en un tiempo determinado.

El editor de scripts integrado

En el interfaz web de adquio se pueden listar todos los scripts, activarlos, desactivarlos y editar su código.

El editor de código incluye un sistema de comprobación de sintaxis, que señala errores gramaticales y da alertas en casos de estructuras no recomendadas o ambiguas. Aún así, no es un sistema infalible y un script que no muestre errores en el editor puede no ser correcto.

Más información en el capítulo sobre el interfaz gráfico.

Tipos de scripts y bloques autogenerados

Existen tres tipos de script en adquio. Todos ellos representan a un fichero que se ejecuta al inicio del sistema, solo se diferencian en función de su origen y el número de bloques que contienen.

  • Scripts genéricos: Listos para ser escritos por el usuario avanzado, con total libertad y un único bloque de código ("principal").
  • Scripts externos: Actualmente, si la adquio está conectada con Equs, el script generado desde Equs por el usuario aparece en esta sección.
  • Scripts de eventos: Para no tener que lidiar con el manejo de eventos en un script genérico, se puede vincular un script a un temporizador o evento desde otras vistas del interfaz web. Tienen tres bloques:
  • Inicialización: Bloque que es ejecutado al arrancar el sistema.
  • Inicio de evento: Bloque de código que se ejecuta cada vez que se dispara el evento, el interfaz ya abstrae la instanciación del manejador e inyecta el código de este bloque.
  • Fin de evento (solo en eventos con duración): Bloque de código que se ejecuta cuando el evento ha finalizado.

Dispositivos y variables

Los dispositivos y variables se acceden desde el objeto devices.

Desde devices se puede operar directamente sobre una variable:

const valor = 2;
devices.write('dispositivo', 'variable', valor);

También se pueden obtener referencias a dispositivos y variables, para ser utilizados más adelante, por ejemplo:

const dispositivo = devices.find('dispositivo');
const variable1 = dispositivo.findVariable('variable1');
const variable2 = devices.findVariable('dispositivo', 'variable2');
variable1.write(variable2.read() + 1);
variable2.write(dispositivo.read('variable3') - 1);

Nota: Si la variable/dispositivo obtenidos no existen, su valor es undefined, debería verificar el valor antes de operar sobre ellos.

Nota: Obtener una variable o dispositivo no garantiza que en el momento de operar estos sigan existiendo o su estado sea funcional. Podríamos obtener un error de "VariableNotFound" sobre una variable ya obtenida.

Eventos

adquio está diseñado basado en eventos, los eventos que son interesantes para el usuario programador se encuentran dentro del objeto events.

Para reaccionar ante un evento, se instancia un manejador para ese evento con la función events.addListener(filtro, manejador), una vez se ejecuta esta función, cada vez que un evento pase el filtro se ejecutará la función manejador que recibe los datos del evento.

Tipos de eventos:

  • Alert: Nueva alerta, expiración o actualización de alerta existente.
  • VariableChange: Una variable ha cambiado de valor.
  • Timer: Detecta eventos programados, configurables desde la interfaz web en la sección Programación.
  • VariableStatusChange: Cambio de estado de una variable.
  • DeviceStatusChange: Cambio de estado de un dispositivo.
  • NewVariablesHistoric: Nuevo conjunto de datos de las variables registrado. Incluye datos "agregados" o la toma de datos en muestras periódicas.

Desde la interfaz gŕafica se simplifica la reación de eventos. De esta forma no es necesario definir a mano el filtro, vea la sección Scripts de eventos para más información.

Definiendo el filtro para eventos

El filtro que selecciona si un manejador ha de ser llamado para determinado evento puede ser de dos tipos:

  • Una función que recibe eventData y resuelve a true o false. Otros valores serán convertidos a true o false según las reglas de JavaScript.
events.addListener(data => data.eventName === 'VariableChange' && data.variableCode.startsWith('UE'), data => {
  // reacción al evento
});
  • Un objeto, se comparan todos los campos que existen en el objeto recibido. Para que el filtro se cumpla, los campos del filtro deben existir en los datos del evento y ser iguales (usando ===):
events.addListener({
  eventName: 'VariableChange',
  variableCode: 'UE_01'
}, data => {
  // reacción al evento
});

Definiendo el manejador

El manejador puede ser cualquier función definida por el usuario, que recibe como único parámetro el objeto del evento. Cada evento lleva un identificador o eventName y campos que dependen del tipo de evento.

En la ayuda lateral de los scripts puede ver la definición de los campos presentes en cada tipo de evento. Los tipos correspondientes finalizan en EventData en su nombre.

Ejemplo de visualización en la ayuda de un EventData

Por ejemplo, un manejador podría comprobar el nuevo valor de una variable y si no cumple un umbral, tratar de escribir un nuevo valor:

events.addListener({
  eventName: 'VariableChange',
  deviceCode: 'UE_01',
  variableCode: 'TSET'
}, data => {
  const variable = devices.find(data.deviceCode, data.variableCode);
  if(!variable) {
    // Registramos el error y salimos del manejador
    logger.error('Falta la variable UE_01#TSET');
    return;
  }
  if(data.newValue < 19) {
    variable.write(19);
  } else if(data.newValue > 25) {
    variables.write(25);
  }
});

Alertas

Desde los scripts de adquio se pueden generar y manejar alertas. Las alertas se mostrarán luego en la interfaz web, en la pestaña de Alertas. Algunas alertas son enviadas a Equs si está activa la integración, es el caso de las alertas de tipo "EqusAlert" y las que contienen alguno de los tags "system" o "auto".

Hacer touch() sobre una alerta actualiza su fecha de última comprobación. El campo expireAfterMs define cuantos milisegundos pasan antes de expirar automáticamente desde que a una alerta se le ejecuta create() o touch() por última vez. Si el campo expireAfterMs no está definido la alerta no expira automaticamente, en ese caso se puede definir un endTime (ya sea en la creación o actualizándola).

El método touchOrCreate() busca si existe una alerta equivalente, si existe ejecuta un touch() sobre ella, si no existe la crea. Dos alertas son equivalentes si los siguientes campos son iguales: type, payload, priority y owner.

Nota: Si una alerta no tiene definido expireAfterMs y no llega nunca a establecerse el valor de endTime, la alerta permanecerá en estado activo indefinidamente. Se recomienda establecer siempre un periodo para expireAfterMs prudencial para evitar esa situación.

Ejemplo:
Crear una alerta de tipo "Personalizado", con los datos especificados. Cada 5 segundos se actualiza con el último valor de la variable dispositivo#variable. Como expireAfterMs es mayor que el tiempo de actualización, la alerta solo expirará si se reinicia el sistema y el intervalo no se ejecuta en los próximos 5 segundos:

setInterval(() => {
  alerts.touchOrCreate({
    priority: 10,
    type: 'Personalizado',
    payload: {
      // El payload puede ser cualquier objeto serializable de JavaScript, se utiliza para verificar si dos alertas son equivalentes (es decir, a la hora de buscar una existente se compara este campo)
      description: 'Alerta personalizada'
    },
    dynamicPayload: {
      // El campo dynamicPayload funciona como un payload que no altera la identidad de la alerta. Sirve para actualizar datos dinámicos de la alerta que queremos que siga siendo la misma (la función touchOrCreate va a actualizarla y no crear una nueva)
      ultimoValor: devices.read('dispositivo', 'variable')
    },
    // Si la alerta no es tocada (touch) en 6 segundos, expira automáticamente
    expireAfterMs: 6000
  });
}, 5000);

Crear una alerta, independientemente de que ya exista una equivalente. Luego expirarla una vez creado:

const alerta = alerts.create({
  priority: 10,
  type: 'Personalizado',
  payload: 'Prueba de creación de alerta',
  expireAfterMs: 1000
});
// Podemos hacer un touch de la nueva alerta
alerts.touch(alerta);
// O forzar su expiración
alerts.expire(alerta);

Campos de alertas

Cuando se define una alerta, se pueden definir los siguientes campos:

Campos obligatorios la alerta no se genera y produce un error si no se definen:

  • priority: número que representa la importancia de la alerta, siendo 0 la mayor prioridad posible (números menores indican mayor prioridad).
  • type: texto que agrupa cada alerta dentro de un tipo común. Puede utilizar cualquier texto, pero se recomienda agrupar alertas similares en el mismo tipo para facilitar su comprensión y búsqueda.

Campos opcionales:

  • expireAfterMs: tiempo en milisegundos que la alerta estará activa desde el último touch() o desde su creación. Una vez transcurrido este tiempo la alerta expira automáticamente, definiendo el campo endTime a el momento de expiración. Nota: Aunque el campo es opcional, se recomienda especificarlo siempre, para evitar crear alertas que nunca expiren, si planea expirarlas manualmente especifique un valor alto que no interfiera con esa intención.
  • payload: datos relativos a la alerta, dependientes del tipo o de la intención del usuario. Permite guardar una cadena de texto o un objeto JS. No puede cambiar ya que forma parte de la identidad de la alerta. Por ejemplo en una alerta de valor bajo, payload contiene la identificación de la variable que causa esa alerta.
  • dynamicPayload: datos de la alerta cambiantes. Es decir, una misma alerta puede cambiar estos valores sin cambiar de identidad. Por ejemplo, una alerta de valor demasiado bajo guarda en dynamicPayload el valor más reciente, de forma que sin cambiar la alerta puede actualizarse el valor.
  • tags: lista de etiquetas. Las etiquetas son textos cortos que clasifican las alertas. Existen dos etiquetas especiales system y equs; system genera alertas críticas del sistema, mostradas en rojo en lugar de amarillo, equs genera alertas que se suben a Equs si está activo en la adquio.
  • endTime: si se especifica, define el momento en el que la alerta deja de estar activa. No se recomienda su uso en la mayoría de casos, ya que existe el campo expireAfterMs para hacer caducar alertas que lleven un tiempo sin suceder. Acepta Date o timestamp.
  • startTime: si no se especifica se establece al momento actual. Momento desde el cual la alerta está activa, definida como Date o como timestamp numérico.
  • condition: ver Alertas condicionales
  • onConditionError y onOutOfService: acción en caso de error, ver Alertas condicionales
  • delay: ver Alertas aplazadas

Alertas condicionales

Se puede definir algunos campos que no forman parte de la alerta en las funciones de create() y touchOrCreate().

El campo condition establece una condición que ha de ser cierta para que se cree/actualice la alerta. La condición puede ser un texto que se evalúa mediante la función calc() o una función, tal como se ve en este ejemplo:

// Crear condición mediante una fórmula en texto
alerts.touchOrCreate({
  priority: 10,
  type: 'CondicionFormula',
  payload: 'Variable temp_exterior del dispositivo sondas mayor que 19',
  expireAfterMs: 30000,
  condition: '$sondas#temp_exterior$ > 19'
});
// Crear condición mediante una función
alerts.touchOrCreate({
  priority: 10,
  type: 'CondicionFuncion',
  payload: 'Puerta entrada abierta',
  expireAfterMs: 30000,
  condition: () => devices.read('puertas', 'entrada'),
  onOutOfService: 'alert'
})

La alerta sólo se crea si la fórmula o la función devuelven valores ciertos.

También se incluyen dos opciones para controlar el comportamiento en caso de errores:

  • onOutOfService especifica qué se hará si evaluar la condición implica acceder a una variable que está fuera de servicio. Si no se especifica el valor es "ignore".
  • onConditionError especifica qué se hará si evaluar la condición produce cualquier otro error no manejado. Si no se especifica es "alert".

Los valores posibles son (En todos los casos la alerta original no se genera y se registra el error en los logs):

  • ignore: descartar el error, sin embargo quedará registrado en los logs.
  • alert: genera una alerta indicando el error sucedido.
  • error: lanza el error para poder ser manejado por el script.

Alertas aplazadas

Las alertas aplazadas permiten indicar además de una condición un periodo de espera (campo delay), de modo que:

  • Si la primera vez que se crea a la alerta la condición se cumple, se aplaza otra comprobación de la condición para el tiempo de espera indicado.
  • Si durante el periodo de espera se vuelve a hacer touchOrCreate() y la condición ya no se cumple la segunda comprobación queda anulada y se descarta la alerta sin ser creada.
  • Si en la comprobación aplazada la condición se cumple, se crea, si no se descarta y anula la espera.

De este modo, si la alerta se genera es porque la condición ha sido cierta al menos:

  • al ser llamada por primera vez en el script (sin tener una espera pendiente) y
  • tras esperar el tiempo indicado y
  • todas las veces en ese periodo que se haya llamado en el script

Nota: Por el modo de funcionamiento, no está permitido indicar el campo delay con el método create() y solo se soporta con touchOrCreate().

El siguiente ejemplo creará una alerta si la variable puertas#entrada es 1 y sondas#entrada menor que 18 durante al menos 10 segundos. Sin embargo tardará al menos 10 segundos en hacerlo. Esta comprobación se realiza cada 5 segundos por el setInterval().

setInterval( () =>
  alerts.touchOrCreate({
    priority: 10,
    type: 'EjemploAplazada',
    payload: 'La puerta lleva abierta 10 segundos y hace frío',
    expireAfterMs: 10000,
    condition: '($puertas#entrada$ === 1) && ($sondas#entrada$ < 18)'
  })
, 5000);

Fórmulas y evaluación

En algunos casos puede ser práctico o más rápido poder definir evaluaciones o condiciones con una notación matemática. Para eso se incorporan las fórmulas a la capacidad de los scripts.

Una fórmula interpreta un texto e intenta evaluarlo, permitiendo representar el valor de una variable de adquio con la notación $código dispositivo#código variable$. Permite operadores mátemáticos comunes y expresiones en EcmaScript, como +, -, <=...

Para crear una fórmula, instánciela utilizando la función formula(), sobre el resultado puede ejecutar eval() para obtener su valor:

const miFormula = formula('$sondas#exterior$ - 21');
const delta = miFormula.eval();
logger.debug('Delta es', delta);

Para no tener que manejar un objeto de Formula puede utilizar el método calc() que devuelve directamente el valor de evaluar la fórmula, por ejemplo podría:

if(calc('$sondas#exterior$ < 15')) {
  logger.debug('Estoy a ' + calc('21 - $sondas#exterior$') + ' grados menos de lo que querría');
}

Nota: No se permiten hacer asignaciones ni escrituras dentro de las fórmulas, fórmulas como $dev#var$ = 5 no son válidas.