WThermostatBeca helper

Helpers for WThermostatBeca.

// ==UserScript==
// @name        WThermostatBeca helper
// @namespace   marcuson
// @description Helpers for WThermostatBeca.
// @match       *://*/thermostat_schedules
// @version     1.1.4
// @author      marcuson
// @license     GPL-3.0-or-later
// @supportURL  https://github.com/marcuson/WThermostatBeca-helpers/issues
// @homepageURL https://github.com/marcuson/WThermostatBeca-helpers
// @require     https://cdn.jsdelivr.net/combine/npm/@violentmonkey/dom@2,npm/@violentmonkey/[email protected]
// @grant       GM.addStyle
// @grant       GM.getValue
// @grant       GM.setValue
// ==/UserScript==

(function () {
'use strict';

var css_248z = "";

var styles = {"btnContainer":"style-module_btnContainer__P6fk3","btnGrouper":"style-module_btnGrouper__6lr6j","btnSpacer":"style-module_btnSpacer__s-37w","btn":"style-module_btn__Bc4TY","small":"style-module_small__KjQen","mr":"style-module_mr__yG-cJ","error":"style-module_error__ABgWN","warning":"style-module_warning__nASzn","success":"style-module_success__ZG5Xf"};
var stylesheet=".style-module_btnContainer__P6fk3{display:flex;justify-content:space-around}.style-module_btnGrouper__6lr6j{display:flex;flex-grow:1;justify-content:space-between}.style-module_btnSpacer__s-37w{margin:0 20px}.style-module_btn__Bc4TY{cursor:pointer;padding-left:20px;padding-right:20px;width:auto}.style-module_btn__Bc4TY.style-module_small__KjQen{font-size:.8rem;height:auto;line-height:normal;padding-left:5px;padding-right:5px}.style-module_btn__Bc4TY.style-module_small__KjQen.style-module_mr__yG-cJ{margin-right:5px}.style-module_error__ABgWN{color:red}.style-module_warning__nASzn{color:orange}.style-module_success__ZG5Xf{color:green}";

function _extends() {
  _extends = Object.assign ? Object.assign.bind() : function (target) {
    for (var i = 1; i < arguments.length; i++) {
      var source = arguments[i];
      for (var key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
          target[key] = source[key];
        }
      }
    }
    return target;
  };
  return _extends.apply(this, arguments);
}

function toast(type, msg, options) {
  const clz = styles[type];
  const st = [(options == null ? void 0 : options.style) || '', css_248z, stylesheet].join('\n');
  return VM.showToast(VM.h("div", {
    class: clz
  }, VM.h("p", null, msg)), _extends({}, options, {
    style: st
  }));
}

function downloadObjectAsJson(exportObj, exportName) {
  const dataStr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(exportObj));
  const downloadAnchorNode = document.createElement('a');
  downloadAnchorNode.setAttribute('href', dataStr);
  downloadAnchorNode.setAttribute('download', exportName + '.json');
  document.body.appendChild(downloadAnchorNode); // required for firefox
  downloadAnchorNode.click();
  downloadAnchorNode.remove();
}

async function askFileToRead() {
  const input = document.createElement('input');
  input.type = 'file';
  const prom = new Promise((res, rej) => {
    input.onchange = () => {
      const file = input.files[0];
      const reader = new FileReader();
      reader.readAsText(file, 'UTF-8');
      reader.onload = readerEvent => {
        const content = readerEvent.target.result;
        res(content);
      };
      reader.onerror = err => {
        rej(err);
      };
    };
  });
  input.setAttribute('style', 'display:none;');
  document.body.appendChild(input); // required for firefox
  input.click();
  const res = await prom;
  input.remove();
  return res;
}

function strReplaceAt(str, index, replacement) {
  return str.substring(0, index) + replacement + str.substring(index + replacement.length);
}

const WORKDAY_PREFIX = 'w';
const DAYOFF1_PREFIX = 'a';
const DAYOFF2_PREFIX = 'u';
const TIME_SUFFIX = 'h';
const TEMP_SUFFIX = 't';

let copiedData = undefined;
function createCopyBtn(fn) {
  return createSmallBtn('Copy', true, fn);
}
function createPasteBtn(fn) {
  return createSmallBtn('Paste', false, fn);
}
function createSmallBtn(text, withMarginRight, fn) {
  const classes = [styles.btn, styles.small];
  if (withMarginRight) {
    classes.push(styles.mr);
  }
  return VM.m(VM.h("button", {
    class: classes.join(' '),
    onclick: fn
  }, text));
}
function addBtnsTableHeaders(copyBtn, pasteBtn, elem) {
  elem.textContent = elem.textContent + ' ';
  elem.appendChild(copyBtn);
  elem.appendChild(pasteBtn);
}
function addButtons() {
  const saveAndRestoreDefaultsBtn = VM.m(VM.h("div", {
    class: styles.btnContainer
  }, VM.h("span", {
    class: styles.btnGrouper
  }, VM.h("button", {
    class: styles.btn,
    onclick: saveDefaults
  }, "Save current as defaults"), VM.h("button", {
    class: styles.btn,
    onclick: restoreDefaults
  }, "Restore defaults")), VM.h("hr", {
    class: styles.btnSpacer
  }), VM.h("span", {
    class: styles.btnGrouper
  }, VM.h("button", {
    class: styles.btn,
    onclick: exportData
  }, "Export"), VM.h("button", {
    class: styles.btn,
    onclick: importData
  }, "Import"))));
  document.querySelector('table.settingstable').before(saveAndRestoreDefaultsBtn);
  const copyWorkDayBtn = createCopyBtn(copyDataFn(WORKDAY_PREFIX));
  const pasteWorkDayBtn = createPasteBtn(pasteDataFn(WORKDAY_PREFIX));
  addBtnsTableHeaders(copyWorkDayBtn, pasteWorkDayBtn, document.querySelector('table.settingstable thead th:nth-child(2)'));
  const copyDayOff1Btn = createCopyBtn(copyDataFn(DAYOFF1_PREFIX));
  const pasteDayOff1Btn = createPasteBtn(pasteDataFn(DAYOFF1_PREFIX));
  addBtnsTableHeaders(copyDayOff1Btn, pasteDayOff1Btn, document.querySelector('table.settingstable thead th:nth-child(3)'));
  const copyDayOff2Btn = createCopyBtn(copyDataFn(DAYOFF2_PREFIX));
  const pasteDayOff2Btn = createPasteBtn(pasteDataFn(DAYOFF2_PREFIX));
  addBtnsTableHeaders(copyDayOff2Btn, pasteDayOff2Btn, document.querySelector('table.settingstable thead th:nth-child(4)'));
}
async function saveDefaults(e) {
  e.preventDefault();
  console.info('Saving current data as default values...');
  const slots = readInputIntoSlots();
  console.debug('Slots read:', slots);
  await GM.setValue(getDefaultsValueKey(), slots);
  toast('success', 'Current values saved as defaults.');
}
async function restoreDefaults(e) {
  e.preventDefault();
  console.info('Restoring default values...');
  const slots = await GM.getValue(getDefaultsValueKey(), undefined);
  if (!slots) {
    toast('error', 'Default values not found, save them first!');
    return;
  }
  writeSlotIntoInputs(slots);
  toast('success', 'Restored default values.');
}
async function exportData(e) {
  e.preventDefault();
  console.info('Exporting current data...');
  const slots = readInputIntoSlots();
  console.debug('Slots read:', slots);
  downloadObjectAsJson(slots, 'wthermostatbeca-schedules');
  toast('success', 'Export completed.');
}
async function importData(e) {
  e.preventDefault();
  console.info('Importing data...');
  const slotsStr = await askFileToRead();
  const slots = JSON.parse(slotsStr);
  writeSlotIntoInputs(slots);
  toast('success', 'Import completed.');
}
function copyDataFn(dayPrefix) {
  return e => {
    e.preventDefault();
    copyData(dayPrefix);
  };
}
function copyData(dayPrefix) {
  console.info('Copying data with day prefix:', dayPrefix);
  copiedData = readInputIntoSlots(dayPrefix);
  console.debug('Data copied:', copiedData);
  toast('success', 'Data copied.');
}
function pasteDataFn(dayPrefix) {
  return e => {
    e.preventDefault();
    pasteData(dayPrefix);
  };
}
function pasteData(dayPrefix) {
  console.info('Pasting data on day prefix:', dayPrefix);
  if (!copiedData) {
    toast('error', 'Copy some data first!');
    return;
  }
  const currentDayPrefix = Object.keys(copiedData)[0];
  const slots = {
    [dayPrefix]: JSON.parse(JSON.stringify(copiedData[currentDayPrefix]))
  };
  Object.keys(slots[dayPrefix]).forEach(slotIdx => {
    const slot = slots[dayPrefix][slotIdx];
    slot.namePrefix = strReplaceAt(slot.namePrefix, 0, dayPrefix);
  });
  console.debug('Pasting slots:', slots);
  writeSlotIntoInputs(slots);
  toast('success', 'Data pasted.');
}
function writeSlotIntoInputs(slots) {
  Object.keys(slots).forEach(dayPrefix => {
    const daySlots = slots[dayPrefix];
    Object.keys(daySlots).forEach(slotIdx => {
      const slot = daySlots[slotIdx];
      const timeInput = document.querySelector(`input[name="${slot.namePrefix}${TIME_SUFFIX}"]`);
      const tempInput = document.querySelector(`input[name="${slot.namePrefix}${TEMP_SUFFIX}"]`);
      timeInput.value = slot.time;
      tempInput.value = slot.temp;
    });
  });
}
function readInputIntoSlots(dayPrefixFilter) {
  const inputs = document.querySelectorAll('input');
  const slots = [...inputs].reduce((obj, x) => {
    const dayPrefix = x.name.substring(0, 1);
    if (dayPrefixFilter && dayPrefix !== dayPrefixFilter) {
      return obj;
    }
    if (!obj[dayPrefix]) {
      obj[dayPrefix] = {};
    }
    const daySlots = obj[dayPrefix];
    const slotIdx = x.name.substring(1, 2);
    const slot = daySlots[slotIdx] || {
      namePrefix: x.name.substring(0, 2)
    };
    if (x.name.endsWith(TIME_SUFFIX)) {
      slot.time = x.value;
    }
    if (x.name.endsWith(TEMP_SUFFIX)) {
      slot.temp = x.value;
    }
    obj[dayPrefix][slotIdx] = slot;
    return obj;
  }, {});
  return slots;
}
function getDefaultsValueKey() {
  return document.location.origin + document.location.pathname + '/slots';
}

// import CSS
document.head.append(VM.m(VM.h("style", null, css_248z)));
document.head.append(VM.m(VM.h("style", null, stylesheet)));

// init app
addButtons();

})();