<|start_filename|>src/widgets/LoadingWidget.js<|end_filename|>
import React, { Component } from 'react';
import './LoadingWidget.css';
import '../App.css';

class LoadingWidget extends Component {

  constructor(props) {
    super(props);
    this.state = {
      failed: false,
      timeout: null
    }
  }

  componentWillMount() {
    this.setState({ timeout:
        window.setTimeout(() => this.setState({failed: true}), 5000) });
  }

  componentWillUnmount() {
    window.clearTimeout(this.state.timeout);
  }

  render() {
    const theme = this.props.theme;
    const style = theme ? {
      background: this.props.readOnly ? theme.unlockedBackground : theme.background,
      color: theme.text
    } : {};
    return (
      <div className="LoadingWidget" style={style}>
        {this.state.failed ? 'Offline ?' : 'Loading...'}
        {this.state.failed && <button
            onClick={this.props.onRemove}>Remove</button>}
      </div>
    );
  }

}

export default LoadingWidget


<|start_filename|>src/widgets/SwitchBlinds.js<|end_filename|>
import React, { Component } from 'react';
import MqttClientSingleton from '../util/MqttClientSingleton'
import JSONClientSingleton from '../util/JSONClientSingleton'

import './SwitchBlinds.css';

class SwitchBlinds extends Component {

  constructor(props) {
    super(props);
    this.mqtt = new MqttClientSingleton();
    this.json = new JSONClientSingleton();
    // nvalue for normal Blinds switch type.
    this.valueOpen = 1;
    this.valueClosed = 3;
    this.valueStopped = 0;
    this.inverted = false;
    switch (this.props.type) {
      case 'Blinds' :
        switch (this.props.device.stype) {
          case 'ASP' :
            this.valueOpen = 0;
            this.valueClosed = 1;
            this.valueStopped = 2;
            break;
          default :
            break;
        }
        break;
      case 'Blinds Inverted' :
        this.inverted = true;
        this.valueOpen = 3;
        this.valueClosed = 1;
        break;
      case 'Venetian Blinds EU' :
        this.valueOpen = 17;
        this.valueClosed = 18;
        break;
      case 'Venetian Blinds US' :
        this.valueOpen = 15;
        this.valueClosed = 16;
        break;
      default:
        break;
    }
  }

  open = () => {
    this.sendValue("Off");
  }

  stop = () => {
    // Stop command isnt's working via MQTT, so we fall back to JSON.
    this.sendValue("Stop", true /* opt_useJSON */);
  }

  close = () => {
    this.sendValue("On");
  }

  sendValue(val, opt_useJSON = false) {
    if (this.props.readOnly) {
      return
    }
    const message = {
      command: "switchlight",
      idx: this.props.idx,
      switchcmd: val
    };
    if (opt_useJSON) {
      message.type = "command"
      message.param = "switchlight";
      this.json.get(message);
    } else {
      this.mqtt.publish(message);
    }
  }

  render() {
    const isOpen = this.props.value === this.valueOpen;
    const isClosed = this.props.value === this.valueClosed;
    const theme = this.props.theme;
    const style = theme ? {
      background: this.props.readOnly ? theme.unlockedBackground : theme.background,
      color: theme.text
    } : {};
    const buttonStyleOpen = theme ? {
      fill: isOpen ? theme.textOn : theme.textOff,
      background: isOpen ? theme.buttonOn : theme.buttonOff
    } : {};
    const buttonStyleStop = theme ? {
      fill: this.props.value === this.valueStopped ? theme.textOn : theme.textOff,
      background: this.props.value === this.valueStopped ? theme.buttonOn : theme.buttonOff
    } : {};
    const buttonStyleClose = theme ? {
      fill: isClosed ? theme.textOn : theme.textOff,
      background: isClosed ? theme.buttonOn : theme.buttonOff
    } : {};
    return (
      <div className="SwitchBlinds" style={style}>
        <h2>{this.props.label}</h2>
        <section>
          <button onClick={this.inverted ? this.close: this.open} className={'blindsOpen' + (isOpen ? ' selected' : '')} style={buttonStyleOpen}><svg className='icon'><use xlinkHref='#arrow-up-drop-circle' /></svg></button>
          <button onClick={this.stop} className={'blindsStop' + (this.props.value === this.valueStopped ? ' selected' : '')} style={buttonStyleStop}><svg className='icon'><use xlinkHref='#stop-circle' /></svg></button>
          <button onClick={this.inverted ? this.open : this.close} className={'blindsClose' + (isClosed ? ' selected' : '')} style={buttonStyleClose}><svg className='icon'><use xlinkHref='#arrow-down-drop-circle' /></svg></button>
        </section>
      </div>
    );
  }

}

export default SwitchBlinds


<|start_filename|>src/widgets/SmokeSensorWidget.js<|end_filename|>
import React, { Component } from 'react';
import GenericWidget from './helpers/GenericWidget';

class SmokeSensorWidget extends Component {

  render() {
    return (
      <GenericWidget icon="led-on"
        isOn={this.props.value > 0}
        value1={this.props.value === 0 ? 'Normal' : 'Fire detected'}
        {...this.props} />
      );
  }
}

export default SmokeSensorWidget


<|start_filename|>src/widgets/WindWidget.js<|end_filename|>
import React, { Component } from 'react';
import GenericWidget from './helpers/GenericWidget';
import './WindWidget.css';

class WindWidget extends Component {

  render() {
    return (
      <GenericWidget class="WindWidget"
          isOn={this.props.speed > 0}
          value1={this.props.direction}
          value2={Number((this.props.speed / 10) * 3.6).toFixed(1)}
          {...this.props}>
        {this.props.layoutWidth > 1 &&
            <svg style={{transform:'rotateZ(' + this.props.bearing +'deg)' }}>
          <use xlinkHref="#navigation"/>
        </svg>}
      </GenericWidget>
    );
  }

}

export default WindWidget


<|start_filename|>src/widgets/WeatherInfoWidget.css<|end_filename|>
.WeatherInfoWidget .visibility::after {
  content: ' km';
}

.WeatherInfoWidget .radiation::after {
  content: ' watt/m2';
}

.WeatherInfoWidget .uv::after {
  content: ' uvi';
}


<|start_filename|>src/widgets/AlertWidget.js<|end_filename|>
import React, { Component } from 'react';
import GenericWidget from './helpers/GenericWidget';
import './AlertWidget.css';

class AlertWidget extends Component {

  render() {
    return (
      <GenericWidget class={'AlertWidget level' + this.props.level} icon="warning"
          value1={this.props.value} {...this.props} />
    );
  }

}

export default AlertWidget


<|start_filename|>src/ThemeSelector.js<|end_filename|>
import React, { Component } from 'react';

class ThemeSelector extends Component {

  onChange = (event) => {
    this.props.onThemeChange(event.target.value);
  }

  render() {
    const themeIds = [];
    for (const themeKey in this.props.themes) {
      if ({}.hasOwnProperty.call(this.props.themes, themeKey)) {
        themeIds.push(themeKey);
      }
    }
    const options = themeIds.map(
      function(themeId) {
        return (
          <option key={themeId} value={themeId}>{themeId}</option>
        );
      }, this);
    return (
      <select className="ThemeSelector" onChange={this.onChange} value={this.props.currentTheme}>{options}</select>
    );
  }

}

export default ThemeSelector


<|start_filename|>src/widgets/WeatherWidget.css<|end_filename|>
.WeatherWidget, .weatherData {
  display: flex;
  height: 100%;
  font-weight: lighter;
}

.weatherData {
  width: 50%;
  padding: 5px 0;
  flex-direction: column;
  justify-content: center;
}

.weatherData:only-child {
  width: 100%;
  align-items: center;
}

.weatherImage {
  width: 50%;
  min-width: 95px;
  min-height: 95px;
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
}

.weather1 {
  background-image: url('../images/1.png');
}

.weather2 {
  background-image: url('../images/2.png');
}

.weather3 {
  background-image: url('../images/3.png');
}

.weather4 {
  background-image: url('../images/4.png');
}

.weatherData > div:nth-child(2) {
  font-size: 1.75em;
  margin-left: 10px;
  margin-bottom: 5px;
}

.weatherData > div {
  overflow: hidden;
  text-overflow: ellipsis;
  margin-left: 10px;
  margin-bottom: 3px;
}

.temp::after {
  content: '°';
}

.baro::after {
  content: ' hPa';
}

.hum::after {
  content: '% hum.';
}

.humonly::after {
  content: '%';
}


<|start_filename|>src/util/MqttClientSingleton.js<|end_filename|>
import mqtt from 'mqtt';

let singletonInstance = null;

const TOPIC_IN = 'domoticz/in';
const TOPIC_OUT = 'domoticz/out';

class MqttClientSingleton {

  /**
   * A basic mqtt client proxy. Works as a singleton. Constructed with a handler
   * function that will be called with 2 params: event type (one of 'status',
   * 'message', 'error'), and an optional data parameter (respectively, a
   * boolean - true means connected, a data object, or an error message).
   **/

  constructor() {
    if (singletonInstance) {
      return singletonInstance;
    }
    this.client = null;
    this.isConnected = false;
    this.eventHandler = function(type, opt_data) {};
    singletonInstance = this;
    return singletonInstance;
  }

  setEventHandler(eventHandler) {
    this.eventHandler = eventHandler;
  }

  connect(brokerUrl, opt_login, opt_password) {
    if (this.client) {
      this.close();
    }
    if (!brokerUrl) {
      return;
    }
    try {
      this.mqttConfig = {};
      if (opt_login  && opt_password) {
        this.mqttConfig = {
          username: opt_login,
          password: <PASSWORD>
        };
      }
      this.client = mqtt.connect(brokerUrl, this.mqttConfig);
    } catch(e) {
      alert('Connection to MQTT broker failed.\n\nPlease make sure the URL is correct and that it accepts WebSocket protocol.');
      console.log('connection failed', e);
      return;
    }
    // Register the listeners.
    this.client.on('connect', () => {
      this.client.subscribe(TOPIC_OUT);
      this.setConnected_(true);
    });
    this.client.on('message', (topic, message) => {
      const data = JSON.parse(message.toString());
      this.eventHandler('message', data);
    });
    this.client.on('error', (error) => {
      console.log('mqtt client error:', error);
      this.eventHandler('error', error);
    });
    this.client.on('close', () => {
      this.setConnected_(false);
    });
  }

  close() {
    this.client.end(true /* force */);
    this.client = null;
    this.setConnected_(false);
  }

  publish(data) {
    if (!this.isConnected) {
      console.log('Cannot publish message, client is not connected', data);
      return;
    }
    this.client.publish(TOPIC_IN, JSON.stringify(data), {qos: 2});
  }

  setConnected_(isConnected) {
    this.isConnected = isConnected;
    this.eventHandler('connected', this.isConnected);
  }
}

export default MqttClientSingleton;


<|start_filename|>src/widgets/SceneWidget.js<|end_filename|>
import React, { Component } from 'react';
import SwitchOnOff from './SwitchOnOff'
import '../App.css';

class SceneWidget extends Component {

  render() {
    const scene = this.props.scene;
    switch (scene.Type) {
      case "Group" :
      case "Scene" :
        return <SwitchOnOff sceneType={scene.Type} idx={scene.idx} label={scene.Name}
            valueText={scene.Status} pushOn={scene.Type === "Scene"}
            {...this.props}/>;
       default:
         break;
    }
    return <div><h3>Unsupported type: {scene.Type}</h3><pre>{JSON.stringify(scene)}</pre></div>;
  }

}

export default SceneWidget


<|start_filename|>src/widgets/DeviceWidget.js<|end_filename|>
import React, { Component } from 'react';
import AlertWidget from './AlertWidget'
import ContactWidget from './ContactWidget'
import CustomSensorWidget from './CustomSensorWidget'
import GasWidget from './GasWidget'
import LuxWidget from './LuxWidget'
import MediaPlayer from './MediaPlayer'
import MeterWidget from './MeterWidget'
import MotionSensorWidget from './MotionSensorWidget'
import PercentWidget from './PercentWidget'
import RainWidget from './RainWidget'
import SmokeSensorWidget from './SmokeSensorWidget'
import SwitchOnOff from './SwitchOnOff'
import SwitchBlinds from './SwitchBlinds'
import SwitchDimmer from './SwitchDimmer'
import SwitchLock from './SwitchLock'
import SwitchRGBW from './SwitchRGBW'
import SwitchSelector from './SwitchSelector'
import TemperatureWidget from './TemperatureWidget'
import TextWidget from './TextWidget'
import ThermostatWidget from './ThermostatWidget'
import WattWidget from './WattWidget'
import WeatherInfoWidget from './WeatherInfoWidget'
import WeatherWidget from './WeatherWidget'
import WindWidget from './WindWidget'
import './DeviceWidget.css';
import '../App.css';

class DeviceWidget extends Component {

  componentWillMount() {
    // Request device specs for widgets that need it.
    if (this.props.deviceSpec) {
      return;
    }
    switch (this.props.device.switchType) {
      case 'Selector' :
        this.props.requestDeviceSpec();
        break;
      case 'Dimmer' :
        if (this.props.device.stype !== 'RGBW') {
          this.props.requestDeviceSpec();
        }
        break;
      default :
        break;
    }
    switch (this.props.device.stype) {
      case 'Custom Sensor' :
        this.props.requestDeviceSpec();
        break;
      default :
        break;
    }
  }

  render() {
    const device = this.props.device;
    switch (device.switchType) {
      case 'Blinds' :
      case 'Blinds Inverted' :
      case 'Venetian Blinds EU' :
      case 'Venetian Blinds US' :
        return <SwitchBlinds idx={device.idx} label={device.name}
            value={device.nvalue} type={device.switchType} {...this.props} />;
      case 'Contact' :
      case 'Door Contact' :
        return <ContactWidget label={device.name} value={device.nvalue}
            {...this.props} />
      case 'Door Lock' :
        return <SwitchLock idx={device.idx} label={device.name}
            value={device.nvalue} {...this.props} />;
      case 'Dimmer' :
        if (device.stype === 'RGBW') {
          return <SwitchRGBW idx={device.idx} label={device.name}
              value={device.nvalue} {...this.props} />;
        }
        return <SwitchDimmer idx={device.idx} label={device.name}
            device={device}
            value={device.nvalue === 0 ? 0 : device.svalue1} {...this.props} />;
      case 'Media Player' :
        return <MediaPlayer idx={device.idx} label={device.name}
            value={device.nvalue}
            playing={device.svalue1}
            {...this.props} />;
      case 'Motion Sensor' :
        return <MotionSensorWidget label={device.name} value={Number(device.nvalue)}
            {...this.props} />
      case 'On/Off' :
        if (device.stype === 'KD101 smoke detector' || (typeof device.description === 'string' && device.description.includes('Reacticz: smoke sensor'))) {
          return <SmokeSensorWidget idx={device.idx} label={device.name}
              value={device.nvalue} {...this.props} />;
        }
        return <SwitchOnOff idx={device.idx} label={device.name}
            value={device.nvalue} {...this.props} />;
      case 'Push Off Button' :
        return <SwitchOnOff idx={device.idx} label={device.name}
            pushOff={true} value={device.nvalue} {...this.props} />
      case 'Push On Button' :
        return <SwitchOnOff idx={device.idx} label={device.name}
            pushOn={true} value={device.nvalue} {...this.props} />
      case 'Selector' :
        return <SwitchSelector idx={device.idx} label={device.name}
            value={device.svalue1}
            levels={device.LevelNames.split('|')}
            useButtons={device.SelectorStyle === '0'}
            {...this.props} />;
      default :
        break;
    }
    switch (device.dtype) {
      case 'General' :
        switch (device.stype) {
          case 'Alert' :
            return <AlertWidget label={device.name} value={device.svalue1}
                level={device.nvalue} {...this.props} />
          case 'Custom Sensor' :
            return <CustomSensorWidget label={device.name} value={device.svalue1}
                {...this.props} />
          case 'kWh' :
            return <WattWidget label={device.name} powerValue={device.svalue1}
                energyValue={device.svalue2} {...this.props} />
          case 'Percentage' :
            return <PercentWidget label={device.name} value={device.svalue1}
                {...this.props} />
          case 'Solar Radiation' :
            return <WeatherInfoWidget label={device.name} radiation={Number(device.svalue1)}
                {...this.props} />
          case 'Text' :
            return <TextWidget label={device.name} value={device.svalue1}
                {...this.props} />
          case 'Visibility' :
            return <WeatherInfoWidget label={device.name} visibility={Number(device.svalue1)}
                {...this.props} />
          default :
            break;
        }
        break;
      case 'Heating' :
      case 'Thermostat' :
        return <ThermostatWidget idx={device.idx} label={device.name}
            value={device.svalue1} {...this.props} />
      case 'P1 Smart Meter' :
        switch (device.stype) {
          case 'Energy' :
            return <WattWidget label={device.name} powerValue={device.svalue5}
                prodValue={device.svalue6}
                {...this.props} />
          case 'Gas' :
            return <GasWidget label={device.name} usage={device.svalue1}
                {...this.props} />
          default :
            break;
        }
        break;
      case 'Humidity' :
        return <TemperatureWidget label={device.name}
            humonly={Number(device.nvalue)}
            humstat={Number(device.svalue1)}
            {...this.props} />
      case 'Lux' :
        return <LuxWidget label={device.name}
            lux={Number(device.svalue1)}
            {...this.props} />
      case 'Rain' :
        return <RainWidget label={device.name}
            rain={Number(device.svalue2).toFixed(1)}
            rate={Number(device.svalue1 / 100 ).toFixed(1)}
            {...this.props} />
      case 'RFXMeter' :
        let unit, value = Number(device.svalue1);
        switch (device.meterType) {
          case 'Gas' :
            return <GasWidget label={device.name} usage={device.svalue1}
                {...this.props} />
          case 'Counter':
            unit = device.ValueUnits;
            break;
          case 'Energy' :
          case 'Energy Generated' :
            value /= 1000;
            unit = 'kWh';
            break;
          case 'Water' :
            value /= 1000;
            unit = 'm3';
            break;
          case 'Unknown':
          default :
            break;
        }
        return <MeterWidget label={device.name} value={value} unit={unit}
            {...this.props} />
      case 'Temp' :
        return <TemperatureWidget label={device.name}
            temp={Number(device.svalue1).toFixed(1)}
            {...this.props} />
      case 'Temp + Humidity' :
        return <TemperatureWidget label={device.name}
            temp={Number(device.svalue1).toFixed(1)}
            hum={Number(device.svalue2)}
            {...this.props} />
      case 'Temp + Humidity + Baro' :
        return <WeatherWidget hidePressure='true' {...this.props} />;
      case 'Usage' :
        switch (device.stype) {
          case 'Electric' :
            return <WattWidget label={device.name} powerValue={device.svalue1}
                {...this.props} />
          default :
            break;
        }
        break;
      case 'UV' :
        return <WeatherInfoWidget label={device.name}
            uv={Number(device.svalue1)}
            {...this.props} />
      case 'Wind' :
        return <WindWidget idx={device.idx} label={device.name}
            bearing={Number(device.svalue1)}
            direction={device.svalue2}
            speed={Number(device.svalue3)}
            {...this.props} />
      default :
        break;
    }
    return <div className="Unsupported">
        <h2>Unsupported type: {device.switchType || device.stype}</h2>
        <textarea defaultValue={JSON.stringify(device)}></textarea></div>;
  }

}

export default DeviceWidget


<|start_filename|>src/widgets/SwitchDimmer.js<|end_filename|>
import React, { Component } from 'react';
import LoadingWidget from './LoadingWidget';
import Slider from './helpers/Slider';
import MqttClientSingleton from '../util/MqttClientSingleton'
import './SwitchDimmer.css';

class SwitchDimmer extends Component {

  constructor(props) {
    super(props);
    this.mqtt = new MqttClientSingleton();
    this.state = {
      localValue: this.props.value,
      debounceTimeoutId: null,
      fadeTimeoutId: null,
      showBar: false
    }
    this.touchTarget = null;
    this.touchStartData = null;
  }

  handleValueChange = (value) => {
    if (value === 0) {
      this.sendOff();
    } else {
      this.sendValue(value);
    }
  }

  sendValue = (value) => {
    const scale = this.props.deviceSpec['MaxDimLevel'] || 100;
    const message = {
      command: 'switchlight',
      idx: this.props.idx,
      switchcmd: 'Set Level',
      level: Math.round(value * scale)
    };
    this.mqtt.publish(message);
  }

  sendOff = () => {
    const message = {
      command: 'switchlight',
      idx: this.props.idx,
      switchcmd: 'Off'
    };
    this.mqtt.publish(message);
  }

  toggle = () => {
    if (this.props.readOnly) {
      return
    }
    let message = {
      command: 'switchlight',
      switchcmd: 'Toggle',
      idx: this.props.idx
    }
    this.mqtt.publish(message);
  }

  render() {
    if (!this.props.deviceSpec) {
      return <LoadingWidget />
    }
    const theme = this.props.theme;
    const style = {
      background: this.props.readOnly ? theme.unlockedBackground : theme.background,
    }
    const buttonStyle = theme ? {
      background: this.props.value === 0 ? theme.buttonOff : theme.buttonOn,
      color: this.props.value === 0 ? theme.textOff : theme.textOn
    } : {};
    const progressStyle = theme ? {
      background: theme.buttonOn,
      color: theme.textOn
    } : {};
    const containerStyle = theme ? {
      background: theme.buttonOff,
      color: theme.textOff
    } : {};
    const maxDimLevel = this.props.deviceSpec['MaxDimLevel'] || 100;
    const currentPct = this.props.value / maxDimLevel * 100;
    progressStyle.transform = 'translateX(' + (100 - currentPct) + '%)';
    const toggleLabel = this.props.device.nvalue === 0 ? 'Off' : 'On';
    return (
      <div className="SwitchDimmer" style={style}>
        <button className="toggle" style={buttonStyle} title={this.props.layoutWidth > 1 ? toggleLabel : this.props.label} onClick={this.toggle}>{this.props.layoutWidth > 1 ? '' : this.props.label}</button>
        {this.props.layoutWidth > 1 && <div className="dimmerSlider">
          <div className="dimmerContainer" style={containerStyle}>
            <div className="dimmerProgress" style={{transform: 'translateX(' + currentPct + '%)'}}>
              <div className="dimmerProgressContent" style={progressStyle}>
                <p>{this.props.label}</p>
              </div>
            </div>
            <p>{this.props.label}</p>
          </div>
          <Slider disabled={this.props.readOnly} throttle={200}
              onChange={this.handleValueChange} value={this.props.value/maxDimLevel} />
        </div>}
      </div>
    );
  }
}

export default SwitchDimmer


<|start_filename|>src/widgets/LuxWidget.js<|end_filename|>
import React, { Component } from 'react';
import GenericWidget from './helpers/GenericWidget';
import './LuxWidget.css';

class LuxWidget extends Component {

  render() {
      return (
      <GenericWidget class="LuxWidget" icon="brightness-4"
        isOn={this.props.lux > 0}
        value1={this.props.lux}
        {...this.props} />
      );
  }
}

export default LuxWidget


<|start_filename|>src/widgets/SwitchOnOff.js<|end_filename|>
import React, { Component } from 'react';
import MqttClientSingleton from '../util/MqttClientSingleton'
import './SwitchOnOff.css';

class SwitchOnOff extends Component {

  constructor(props) {
    super(props);
    this.mqtt = new MqttClientSingleton();
  }

  getPushCommand = () => {
    if (this.props.pushOn) {
      return 'On'
    } else if (this.props.pushOff) {
      return 'Off'
    }
    return 'Toggle';
  }

  handleClick = (event) => {
    if (this.props.readOnly) {
      return
    }
    const message = {
      command: this.props.sceneType ? 'switchscene' : 'switchlight',
      idx: parseInt(this.props.idx, 10),
      switchcmd: this.getPushCommand()
    };
    this.mqtt.publish(message);
    this.mqtt.publish({'command': 'getdeviceinfo', 'idx': this.props.idx });
  }

  getButtonStyle(swapStyles = false) {
    const theme = this.props.theme;
    if (!theme) {
      return {};
    }
    const style = {
      color: swapStyles ? theme.textOff : theme.textOn,
      fill: swapStyles ? theme.textOff : theme.textOn,
      background: this.props.readOnly ? theme.unlockedBackground : ( swapStyles ? theme.buttonOff : theme.buttonOn )
    }
    const gradient = 'repeating-linear-gradient(-45deg, _corner, _corner 20%, _bg 20%, _bg 80%, _corner 80%, _corner 100%)';
    if (this.props.pushOn && this.props.sceneType) {
      style.color = theme.textMixed;
      style.background = this.props.readOnly ? theme.unlockedBackground : gradient.replace(/_corner/g, theme.buttonMixed).replace(/_bg/g, theme.buttonOff);
      return style;
    }
    if (this.props.valueText === "Mixed") {
      style.color = theme.textMixed;
      style.background = this.props.readOnly ? theme.unlockedBackground : gradient.replace(/_corner/g, theme.buttonOn).replace(/_bg/g, theme.buttonOff);
    }
    if (this.props.valueText === "Off" || this.props.value === 0) {
      style.color = swapStyles ? theme.textOn : theme.textOff;
      style.fill = swapStyles ? theme.textOn : theme.textOff;
      style.background =  this.props.readOnly ? theme.unlockedBackground : ( swapStyles ? theme.buttonOn : theme.buttonOff );
    }
    return style;
  }

  render() {
    const valueText = this.props.valueText || (this.props.value === 0 ? 'Off' : 'On');
    return (
      <button className="switch" style={this.getButtonStyle()} onClick={this.handleClick} title={valueText}>{this.props.label}</button>
    );
  }

}

export default SwitchOnOff


<|start_filename|>src/widgets/LuxWidget.css<|end_filename|>
.LuxWidget .value1::after {
  content: ' lux';
}


<|start_filename|>src/widgets/helpers/GenericWidget.js<|end_filename|>
import React, { Component } from 'react';
import sanitizeHtml from 'sanitize-html';
import './GenericWidget.css';

class GenericWidget extends Component {

  sanitize(htmlString) {
    return {__html: sanitizeHtml(htmlString, {
      allowedTags: sanitizeHtml.defaults.allowedTags.concat([ 'img' ])})};
  }

  render() {
    const theme = this.props.theme;
    const style = theme ? {
      background: this.props.readOnly ?
          theme.unlockedBackground : theme.background,
      color: theme.text,
      fill: this.props.isOn === undefined ? '' :
          (this.props.isOn ? theme.buttonOn : theme.buttonOff)
    } : {};
    const safeValue1 = this.sanitize(this.props.value1);
    const safeValue2 = this.props.value2 === undefined ?
        null : this.sanitize(this.props.value2);
    return (
      <div className={'GenericWidget '+ this.props.class} style={style}>
        {this.props.icon && this.props.layoutWidth > 1 &&
            <svg><use xlinkHref={'#' + this.props.icon}/></svg>}
        {this.props.children}
        <section>
          <div>{this.props.label}</div>
          <h2 className={this.props.value1Class ?
              this.props.value1Class : "value1"}
              dangerouslySetInnerHTML={safeValue1}></h2>
          {safeValue2 && <div className={this.props.value2Class ?
              this.props.value2Class : "value2"}
              dangerouslySetInnerHTML={safeValue2}></div>}
        </section>
      </div>
    );
  }

}

export default GenericWidget


<|start_filename|>src/widgets/PercentWidget.css<|end_filename|>

.PercentWidget h2::after {
  content: '%';
}

.PercentWidget .doughnut {
  transform: rotate(-90deg);
}

.PercentWidget .doughnut circle {
  stroke-dashoffset: 0;
  stroke-width: 40px;
  fill: transparent;
  transition: stroke-dashoffset .3s ease-out;
}

.PercentWidget .doughnut .bar {
  transform: translateZ(0);
}


<|start_filename|>src/AboutView.test.js<|end_filename|>
import React from 'react';
import ReactDOM from 'react-dom';
import { mount } from 'enzyme';
import AboutView from './AboutView';

it('renders without crashing', () => {
  const wrapper = mount(<AboutView />);
});


<|start_filename|>src/util/ConfigStorageHelper.js<|end_filename|>
import LocalStorage from './LocalStorage'

class ConfigStorageHelper {

  constructor() {
    this.configs_ = null;
    this.store = new LocalStorage();

    // Remove this after a few weeks:
    this.migrateConfigs();
  }

  getConfigs() {
    if (!this.configs_) {
      this.configs_ = this.store.read('configs') || [];
    }
    return this.configs_;
  }

  setConfigs(configs) {
    this.store.write('configs', configs);
    this.configs_ = configs;
  }

  getConfig(id) {
    const configs = this.getConfigs();
    return configs[id];
  }

  getNextId(currentId, opt_prev = false) {
    const configs = this.getConfigs();
    if (configs.length <= 1) {
      return null;
    }
    let configId;
    if (opt_prev) {
      configId = currentId === 0 ? configs.length - 1 : currentId - 1;
    } else {
      configId = currentId === (configs.length - 1) ? 0 : currentId + 1;
    }
    return configId;
  }

  storeConfigChange(configId, partialConfigObject) {
    const configs = this.getConfigs().slice(0);
    const modifiedConfig = configs[configId] || {};
    const newConfig = Object.assign(modifiedConfig, partialConfigObject);
    configs[configId] = newConfig;
    this.setConfigs(configs);
  }

  deleteConfig(configId) {
    const configs = this.getConfigs().slice(0);
    configs.splice(configId, 1);
    this.setConfigs(configs);
  }

  addConfig(opt_config) {
    const configs = this.getConfigs().slice(0);
    const newId = configs.length;
    const newConfig = opt_config || {
      name: 'Dashboard ' + newId,
      layout: [],
      whitelist: []
    };
    configs.push(newConfig);
    this.setConfigs(configs);
    return newId;
  }

  migrateConfigs() {
    const allLayouts = this.store.getKeys().filter(function (propertyName) {
      return propertyName.indexOf("layout") === 0;
    });
    if (allLayouts.length === 0 || this.getConfigs().length > 0) {
      // No migration required.
      return;
    }
    const configs = [];
    for (let i = 0; i < allLayouts.length; i++) {
      const id = allLayouts[i].substring(6);
      let config = {};
      // Compatibility mode: attempt to load from old system and migrate data.
      const layout = this.store.read('layout' + id);
      if (layout) {
        config.layout = layout;
        this.store.delete('layout' + id);
      }
      const whitelist = this.store.read('whitelist' + id);
      if (whitelist) {
        config.whitelist = whitelist;
        this.store.delete('whitelist' + id);
      }
      if (config.whitelist || config.layout) {
        config.name = 'Dashboard' + (id ? ' ' + id : '');
        configs.push(config);
      }
    }
    console.log('Migrated ' + allLayouts.length + ' layout configuration(s).');
    this.store.write('configs', configs);
    document.location.reload();
  }

}

export default ConfigStorageHelper;


<|start_filename|>src/widgets/TemperatureWidget.js<|end_filename|>
import React, { Component } from 'react';
import GenericWidget from './helpers/GenericWidget';
import './TemperatureWidget.css';

class TemperatureWidget extends Component {

  render() {
    if (this.props.humonly !== undefined) {
      const humidityStatus = ((this.props.humstat === 0) ? 'Normal' : (this.props.humstat === 1) ? 'Comfortable' : (this.props.humstat === 2) ? 'Dry' : 'Wet');
      return (
      <GenericWidget class="TemperatureWidget" icon="water-percent"
        isOn={this.props.humstat > 2}
        value1={this.props.humonly}
        value1Class="humonly"
        value2={humidityStatus}
        {...this.props} />
      );
    }
      return (
      <GenericWidget class="TemperatureWidget" icon="thermometer-lines"
        isOn={this.props.temp > 19.9}
        value1={this.props.temp}
        value1Class="temp"
        value2={this.props.hum}
        value2Class="hum"
        {...this.props} />
      );
  }

}

export default TemperatureWidget


<|start_filename|>src/DeviceListView.js<|end_filename|>
import React, { Component } from 'react';
import RoomplanSelector from './RoomplanSelector'
import JSONClientSingleton from './util/JSONClientSingleton'

import './DeviceListView.css';

class DeviceListView extends Component {

  constructor(props) {
    super(props);
    this.state = {
      deviceTypes: [],
      devices: {},
      filter: "",
      showUnused: false
    };
    this.json = new JSONClientSingleton();
  }

  handleListChange = (event) => {
    const checkboxEl = event.target;
    if (checkboxEl.checked && this.props.idxWhitelist.indexOf(checkboxEl.value) < 0) {
      // Add device
      this.props.onWhitelistChange(this.props.idxWhitelist.concat([checkboxEl.value]));
    } else {
      // Remove device
      const result = this.props.idxWhitelist.slice(0);
      result.splice(this.props.idxWhitelist.indexOf(checkboxEl.value), 1);
      this.props.onWhitelistChange(result);
    }
  }

  handleUnusedFilterChange = (event) => {
    const checkboxEl = event.target;
    this.setState({showUnused: checkboxEl && checkboxEl.checked});
  }

  handleNameChange = (event) => {
    const name = event.target.value;
    this.props.onNameChange(name);
  }

  handleFilterChange = (event) => {
    const filter = event.target.value;
    this.setState({filter: filter});
  }

  clearFilter = () => {
    this.setState({filter: ""});
  }

  componentDidMount() {
    this.json.getAllDevices(this.addDevices);
    // We need to poll scenes separately because for some reason, when calling 
    // JSON API for type=devices without specifying the used=true param (as 
    // getAllDevices above does), scenes are omitted.
    this.json.getAllScenes(this.addDevices);
  }

  addDevices = (data) => {
    const devices = Object.assign({}, this.state.devices);
    const deviceTypes = this.state.deviceTypes;
    for (let i = 0 ; i < data.result.length; i++) {
      const deviceType = data.result[i]['Type'];
      if (!devices[deviceType]) {
        devices[deviceType] = [];
        deviceTypes.push(deviceType);
      }
      devices[deviceType].push(data.result[i]);
    }
    this.setState({deviceTypes: deviceTypes.sort(), devices: devices});
  }

  getUid(device) {
    switch(device.Type) {
      case 'Scene':
        return 's|' + device.idx;
      case 'Group':
        return 'g|' + device.idx;
      default:
        return 'd|' + device.idx;
    }
  }

  renderDeviceTypeSection(type) {
    const devices = this.state.devices[type];
    let showSection = false;
    const list = devices.sort((a, b) => b.Name >= a.Name ? -1 : 1).map(
        function(device) {
          const id = this.getUid(device);
          if (escape(device.Name).toLowerCase().match(escape(this.state.filter).toLowerCase())) {
            if (!this.state.showUnused && device.Used === 0) {
              return null;
            }
            showSection = true;
            return (
              <li key={id} className={device.Used === 0 ? "unused" : ""}><label><input type="checkbox" value={id} onChange={this.handleListChange} checked={this.props.idxWhitelist.indexOf(id) >= 0}/>{device.Name}</label></li>
            );
          } else {
            return null;
          }
        }, this);
    if (!showSection) {
      return null;
    }
    return (
      <section key={type}>
        <h3>{type}</h3>
        <ul>{list}</ul>
      </section>
    )
  }

  render() {
    const sections = [];
    for (let i = 0; i < this.state.deviceTypes.length; i++) {
      sections.push(this.renderDeviceTypeSection(this.state.deviceTypes[i]));
    };

    return (
      <div className="DeviceListView">
        <p><label>
          Dashboard name:&nbsp;
          <input type="text" value={this.props.name} name="name" placeholder="Name" onChange={this.handleNameChange} />
        </label></p>
        <span>Tick the devices to show in this dashboard</span>
        <RoomplanSelector needConfirm={this.props.idxWhitelist &&
            this.props.idxWhitelist.length > 0}
            onWhitelistChange={this.props.onWhitelistChange}/>
        <section className="searchbar">
          <div>
            <input type="text" value={this.state.filter} name="filter" placeholder="Search devices" onChange={this.handleFilterChange} /><button onClick={this.clearFilter}>Clear</button>
          </div>
          <div>
            <input type="checkbox" name="unused" id="unused" onChange={this.handleUnusedFilterChange} checked={this.state.showUnused}/><label htmlFor="unused">Show unused</label>
          </div>
        </section>
        {sections}
      </div>
    );
  }
}

export default DeviceListView;


<|start_filename|>src/AboutView.js<|end_filename|>
import React, { Component } from 'react';
import LZString from 'lz-string';
import ThemeSelector from './ThemeSelector';
import appInfo from './ext/package.json';
import select from 'select';
import {TYPE as SCREENSAVER_TYPE} from './Screensaver';

import icon from './ext/icon_64.png';
import './AboutView.css';

const DEFAULT_SCREENSAVER_DELAY_SEC = 60;
const DEFAULT_SCREENSAVER_CONFIG = { delay: -1, type: SCREENSAVER_TYPE.BLANK };

class AboutView extends Component {
  constructor(props) {
    super(props);
    this.state = {
      copyResultTimeoutId: null,
      copyResult: null,
      exportDashboard: false,
      exportUrl: ''
    }
  }

  componentWillUnmount() {
    window.clearTimeout(this.state.copyResultTimeoutId);
  }

  handleDashboardCheckChange = (event) => {
    this.setState({
      exportDashboard: event.target.checked,
    });
  }

  handleZoomChange = (event) => {
    this.props.onZoomChange && this.props.onZoomChange(event.target.value);
  }

  handleToggleScreensaver = (event) => {
    const newConfig = Object.assign({}, this.props.screensaverConfig || DEFAULT_SCREENSAVER_CONFIG);
    newConfig.delay = event.target.checked ? DEFAULT_SCREENSAVER_DELAY_SEC : -1;
    this.props.onScreensaverConfigChange && this.props.onScreensaverConfigChange(newConfig);
  }

  handleChangeScreensaverDelay = (event) => {
    const newConfig = Object.assign({}, this.props.screensaverConfig || DEFAULT_SCREENSAVER_CONFIG);
    newConfig.delay = event.target.value;
    this.props.onScreensaverConfigChange && this.props.onScreensaverConfigChange(newConfig);
  }

  handleChangeScreensaverType = (event) => {
    const newConfig = Object.assign({}, this.props.screensaverConfig || DEFAULT_SCREENSAVER_CONFIG);
    newConfig.type = event.target.value;
    this.props.onScreensaverConfigChange && this.props.onScreensaverConfigChange(newConfig);
  }

  generateExportUrl = () => {
    const params = {
      s: this.props.appState && this.props.appState.serverConfig,
      t: this.props.appState && this.props.appState.themeId
    };
    if (this.state.exportDashboard) {
      params.w = this.props.appState.whitelist;
      const lightLayout = this.props.appState.layout.concat([]);
      for (let i = 0; i < lightLayout.length; i++) {
        delete lightLayout[i].moved;
        delete lightLayout[i].static;
      }
      params.l = lightLayout;
      params.n = this.props.configName;
    }
    const url = document.location.href.split('?')[0].replace('#','') + '#' +
        LZString.compressToEncodedURIComponent(JSON.stringify(params));
    return url;
  }

  handleFocus = (event) => {
    select(event.target);
  }

  copyUrl = () => {
    global.clearTimeout(this.state.copyResultTimeoutId);
    select(this.refs.url);
    let succeeded = false;
    try {
      succeeded = document.execCommand('copy');
    }
    catch (err) {
      console.log('Unable to copy', err);
    }
    this.refs.url.blur();
    this.setState({
      copyResult: succeeded ? 'Copied!' : 'Failed! Use Ctrl+C',
      copyResultTimeoutId:
          global.setTimeout(() => { this.setState({ copyResult: null }) }, 3000)
    });
  }

  render() {
    const url = this.generateExportUrl();
    const screensaverConfig = this.props.screensaverConfig || DEFAULT_SCREENSAVER_CONFIG;
    const screensaverOn = screensaverConfig.delay >= 0;
    return (
      <div className="AboutView">
        <div className="aboutHeader">
          <img src={icon} alt="Reacticz logo"/>
          <div>
            <h1>Reacticz v{appInfo.version}</h1> - <a href="https://github.com/domoticz/Reacticz" target="_blank" rel="noopener noreferrer">GitHub</a>
            <aside>A minimalistic Domoticz dashboard</aside>
          </div>
        </div>
        <section>
          <h2>Interface settings</h2>
          <p>Color theme: <ThemeSelector themes={this.props.themes} currentTheme={this.props.appState && this.props.appState.themeId} onThemeChange={this.props.onThemeChange} /></p>
          <p>Font size: <span className="zoomlabel">Smaller</span><input className="zoomrange" type="range" min="0.7" max="1.3" value={this.props.zoom} step="0.1" onChange={this.handleZoomChange}/><span className="zoomlabel">Bigger</span></p>
          <p><label htmlFor="toggleScreensaver">Screensaver: </label><input id="toggleScreensaver" type="checkbox" onChange={this.handleToggleScreensaver}
                  checked={screensaverOn} /> 
              {screensaverOn && <span><input type="number" max="3600" min="1" step="1" value={screensaverConfig.delay} id="delayInput" onChange={this.handleChangeScreensaverDelay} /> <label htmlFor="delayInput">seconds</label>,&nbsp;
              <label htmlFor="screensaverType">type: </label><select id="screensaverType" onChange={this.handleChangeScreensaverType} value={screensaverConfig.type}>
                <option value={SCREENSAVER_TYPE.BLANK}>Blank</option>
                <option value={SCREENSAVER_TYPE.CLOCK}>Analog clock</option>
              </select></span>}
          </p>
          <p><a href=".">Reload</a></p>
        </section>
        <section>
          <h2>Export configuration</h2>
          <p>To clone your settings to another device, share the URL below.</p>
          <div className="exportOptions">
            <label>
              <input type="checkbox" checked={this.state.exportDashboard} onChange={this.handleDashboardCheckChange} /> include dashboard layout{this.props.multiConfig && ' (' + this.props.configName + ')'}
            </label>
          </div>
          <div className="exportUrl">
            <input type="text" className="url" ref='url' value={url} onFocus={this.handleFocus} readOnly />
            <button onClick={this.copyUrl}>{this.state.copyResult || 'Copy to Clipboard'}</button>
          </div>
          {this.props.multiConfig && this.state.exportDashboard && <em>Tip: repeat the operation with your other dashboards to add them too (they won't overwrite each other).</em>}
        </section>
      </div>
    );
  }

}

export default AboutView


<|start_filename|>src/widgets/WattWidget.css<|end_filename|>
.WattWidget .value1::after {
  content: ' W';
}

.WattWidget .value2::after {
  content: ' kWh';
}

.WattWidget.withProd .value2::after {
  content: ' W (prod)';
}


<|start_filename|>src/widgets/ThermostatWidget.js<|end_filename|>
import React, { Component } from 'react';
import JSONClientSingleton from '../util/JSONClientSingleton'
import './ThermostatWidget.css';

class ThermostatWidget extends Component {

  constructor(props) {
    super(props);
    this.json = new JSONClientSingleton();
    this.state = {
      settingValue: false,
      targetValue: parseFloat(this.props.value, 10),
      throttleId: null
    };
  }

  decreaseSetpoint = () => {
    this.updateSetpoint(-0.5);
  }

  increaseSetpoint = () => {
    this.updateSetpoint(0.5);
  }

  updateSetpoint(delta) {
    if (this.props.readOnly) {
      return
    }
    window.clearTimeout(this.state.throttleId);
    let targetValue = this.state.settingValue ? this.state.targetValue :
        parseFloat(this.props.value);
    targetValue += delta;
    this.setState({
      settingValue: true,
      targetValue: targetValue,
      throttleId: window.setTimeout(() => this.emitSetpointUpdate(), 1000)
    });
  }

  emitSetpointUpdate() {
    const message = {
      type: 'command',
      param: 'udevice',
      idx: this.props.idx,
      nvalue: 0,
      svalue: this.state.targetValue
    };
    this.json.get(message, () => {
      this.setState({settingValue: false})
    });
  }

  render() {
    const updating = this.state.settingValue ?
        this.state.targetValue !== parseFloat(this.props.value, 10) : false
    const displayValue = updating ? Number(this.state.targetValue).toFixed(1) :
        parseFloat(this.props.value, 10);
    const theme = this.props.theme;
    const style = theme ? {
      background: this.props.readOnly ? theme.unlockedBackground : theme.background,
      color: theme.text
    } : {};
    const buttonStyle = theme ? {
      background: theme.buttonOff,
      color: theme.textOff
    } : {};
    return (
      <div className="ThermostatWidget" style={style}>
        <h2>{this.props.label}</h2>
        <div className="controls">
          <button className="switch minus" style={buttonStyle}
                  onClick={this.decreaseSetpoint}>-</button>
          <div className={updating ? 'blink' : ''}>{displayValue}</div>
          <button className="switch plus" style={buttonStyle}
                  onClick={this.increaseSetpoint}>+</button>
        </div>
      </div>
    );
  }

}

export default ThermostatWidget


<|start_filename|>src/Screensaver.css<|end_filename|>
.Screensaver {
  position: fixed;
  top: 0px;
  left: 0px;
  right: 0px;
  bottom: 0px;
  background-color: black;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  animation: screensaverFade 1s linear forwards;
  opacity: 0;
}

@keyframes screensaverFade {
  from { opacity: 0; }
  to { opacity: 1; }
}

.Clock .react-clock__second-hand {
  transition: transform cubic-bezier(0.68, 0, 0.27, 1.55) 0.2s;
}

.Clock .react-clock__face {
  border: none;
}

.Clock .react-clock__second-hand__body:after {
  content: '';
  display: block;
  width: 3vmin;
  height: 3vmin;
  position: absolute;
  top: 0;
  left: 50%;
  background-color: red;
  border-radius: 50%;
  transform: translateX(-50%);
}

.Clock .react-clock__second-hand__body:before {
  content: '';
  display: block;
  width: 1vmin;
  height: 1vmin;
  position: absolute;
  bottom: 22%;
  left: 50%;
  background-color: red;
  border-radius: 50%;
  transform: translateX(-50%) translateY(-50%);
}

<|start_filename|>src/widgets/helpers/Slider.js<|end_filename|>
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import './Slider.css';

const HORIZONTAL_THRESHOLD_RAD = 1;

class Slider extends Component {

  constructor(props) {
    super(props);
    this.state = {
      throttleTimeoutId: null,
      touchStartData: null,
      ignoringMove: false,
      fadeTimeoutId: null,
      showBar: false,
      value: this.props.value
    }
    this.touchTarget = null;
  }

  componentDidMount = () => {
    this.touchTarget = ReactDOM.findDOMNode(this);
  }

  updateValue = (value) => {
    this.setState({showBar: true});
    this.setState({value: value});
    if (this.props.throttle) {
      global.clearTimeout(this.state.throttleTimeoutId);
      this.setState({
        throttleTimeoutId: global.setTimeout(
            this.emitChange, this.props.throttle)});
    } else {
      this.emitChange(value);
    }
    global.clearTimeout(this.state.fadeTimeoutId);
    this.setState({fadeTimeoutId: global.setTimeout(this.fadeBar, 1000)});
  }

  emitChange = (opt_value) => {
    this.props.onChange(
        opt_value !== undefined ? opt_value : this.state.value);
  }

  onTouchStart = (event) => {
    if (this.props.disabled) {
      return;
    }
    this.setState({touchStartData: {
      x: event.touches[0].pageX,
      y: event.touches[0].pageY,
      rect: this.touchTarget.getBoundingClientRect()
    }, ignoringMove: false});
  }

  onTouchMove = (event) => {
    if (this.props.disabled || this.state.ignoringMove) {
      return;
    }
    const touch = event.changedTouches[0];
    const angle = Math.atan2(
        touch.pageY - this.state.touchStartData.y,
        touch.pageX - this.state.touchStartData.x);
    const isVertical =
        (Math.abs(Math.abs(angle) - Math.PI / 2)) <= HORIZONTAL_THRESHOLD_RAD;
    if (isVertical) {
      this.setState({ignoringMove: true});
      return;
    }
    const pct = (touch.pageX - this.state.touchStartData.rect.left) /
        this.state.touchStartData.rect.width;
    // Clamp value in [0, 1].
    const targetValue = Math.min(1, Math.max(pct, 0));
    if (this.state.value !== targetValue) {
      this.updateValue(targetValue);
    }
  }

  onTouchEnd = () => {
    this.setState({touchStartData: null});
  }

  onClick = (event) => {
    if (this.props.disabled) {
      return;
    }
    const rect = this.touchTarget.getBoundingClientRect();
    let value = (event.pageX - rect.left) / rect.width;
    // Make slightly larger touch target for value 1 (it's hard to hit the
    // edge). Border tolerance can be anywhing between zero and 1 (in which case
    // the slider basically becomes a toggle).
    const edgeTolerance = this.props.edgeTolerance !== undefined ?
        this.props.edgeTolerance / 2 : 0.06;
    if (value >= 1 - edgeTolerance) {
      value = 1;
    }
    this.updateValue(value);
  }

  fadeBar = () => {
    this.setState({showBar: false});
  }

  render() {
    const valuePct = this.state.value * 100;
    return (
      <div className="Slider" onClick={this.onClick}
            onTouchStart={this.onTouchStart} onTouchMove={this.onTouchMove}
            onTouchEnd={this.onTouchEnd}>
        <div className="bar"
            style={{
              transform: 'translateX(' + valuePct + '%)',
              transition: this.state.touchStartData ? 'none' : undefined,
              opacity: this.state.showBar ? 1 : 0}}>
        </div>
      </div>
    );
  }
}

export default Slider


<|start_filename|>src/util/JSONClientSingleton.js<|end_filename|>
import axios from 'axios';

let singletonInstance = null;

const GET_ALL_DEVICES = {type: 'devices', filter: 'all', order: 'Name'};
const GET_ALL_SCENES = {type: 'scenes'};

class JSONClientSingleton {

  /**
   * A basic JSON API client proxy. Works as a singleton.
   **/

  constructor() {
    if (singletonInstance) {
      return singletonInstance;
    }
    this.throttleTimeout_ = null;
    this.debounceErrors_ = false;
    this.isConnected = false;
    this.eventHandler = function(type, opt_data) {};
    singletonInstance = this;
    return singletonInstance;
  }

  setEventHandler(eventHandler) {
    this.eventHandler = eventHandler;
  }

  setServerUrl(url, opt_login, opt_password) {
    this.serverUrl = url;
    this.axiosConfig = {};
    if (opt_login && opt_password) {
      this.axiosConfig.auth = {
        username: opt_login,
        password: <PASSWORD>
      };
    }

    // Check if connection is ok with parameters
    axios.head(this.serverUrl+ '/json.htm', this.axiosConfig).then(response => {
      this.setConnected_(true);
    }).catch(error => {
      this.setConnected_(false);
    });
  }

  getAllDevices(callback) {
    this.get(GET_ALL_DEVICES, callback);
  }

  getAllScenesThrottled = (callback) => {
    if (this.throttleTimeout_) {
      return;
    }
    this.getAllScenes(callback);
    this.throttleTimeout_ = global.setTimeout(
      () => { this.throttleTimeout_ = null }, 500);
  }

  getAllScenes = (callback) => {
    this.get(GET_ALL_SCENES, callback);
  }

  objectToQuery(data) {
    const keyvals = [];
    for (const key in data) {
      if ({}.hasOwnProperty.call(data, key)) {
        keyvals.push(key + '=' + encodeURIComponent(data[key]));
      }
    }
    return keyvals.join('&');
  }

  get(queryData, opt_callback = function(data) {
    if (data.status !== 'OK') {
      console.log('JSON query failed', queryData);
    }
  }) {
    if (!this.serverUrl) {
      console.log('Server URL is not set, please check the settings');
      return;
    }
    axios.get(this.serverUrl+ '/json.htm?' + this.objectToQuery(queryData), this.axiosConfig).then(response => {
      opt_callback(response.data);
    }).catch(error => {
      if (!this.debounceErrors_) {
        alert('Unable to reach Domoticz server.\n\nPlease check your Domoticz server URL in the settings and make sure Domoticz is online.');
        window.setTimeout(() => (this.debounceErrors_ = false), 3000);
        this.debounceErrors_ = true;
      }
      console.log('Unable to reach Domoticz', error);
    });
  }

  setConnected_(isConnected) {
    this.isConnected = isConnected;
    this.eventHandler('connected', this.isConnected);
  }

}

export default JSONClientSingleton;


<|start_filename|>src/widgets/SwitchRGBW.css<|end_filename|>
.SwitchRGBW {
  display: flex;
  height: 100%;
}

.SwitchRGBW .toggle {
  width: 29%;
  height: 100%;
  min-width: 80px;
  border: none;
  padding: 5px;
}

.SwitchRGBW .toggle:only-child {
  width: 100%;
  border: none;
}

.SwitchRGBW button {
  display: block;
  margin: 0;
  transition: all .3s ease-out;
  font-size: 1.12em;
  font-weight: lighter;
  text-overflow: ellipsis;
  overflow: hidden;
}

.SwitchRGBW button:active {
  transform: scale(0.9);
  outline: none;
}

.SwitchRGBW button:focus {
  outline: none;
}

.SwitchRGBW .colorSelect {
  display: flex;
  flex-direction: column;
  width: 100%;
}

.SwitchRGBW .hue {
  position: relative;
  height: 100%;
  margin: 10px 0 0 10px;
  flex: 1;
  background: linear-gradient(to right, rgb(255, 0, 0) 0%, rgb(255, 255, 0) 17%, rgb(0, 255, 0) 33%, rgb(0, 255, 255) 50%, rgb(0, 0, 255) 67%, rgb(255, 0, 255) 83%, rgb(255, 0, 0) 100%);
  cursor: pointer;
  opacity: .9;
  filter: saturate(85%);
}

.SwitchRGBW .swatch {
  position: relative;
  display: flex;
  height: 100%;
  margin: 0 0 0 10px;
  flex: 1;
}

.SwitchRGBW .swatch * {
  display: flex;
  flex: 1;
  padding: 0;
  border: none;
  background-color: transparent;
}

.SwitchRGBW button.white {
  width: 100%;
  background-color: white;
  border: 2px solid transparent;
}

.SwitchRGBW button.white:active {
  opacity: 0.9;
}



<|start_filename|>src/widgets/SwitchLock.js<|end_filename|>
import React from 'react';
import SwitchOnOff from './SwitchOnOff';
import './SwitchLock.css';
import './SwitchOnOff.css';

class SwitchLock extends SwitchOnOff {

  render() {
    const valueText = this.props.value === 0 ? 'Unlocked' : 'Locked';
    const iconId = this.props.value === 0 ? 'exit-to-app' : 'lock-outline';
    return (
      <button className="switch SwitchLock" style={this.getButtonStyle(true)} onClick={this.handleClick} title={valueText}>
      	{this.props.layoutWidth > 1 &&
      		<svg className="lockicon"><use xlinkHref={'#' + iconId}/></svg>}
		<div>{this.props.label}</div>
      </button>
    );
  }

}

export default SwitchLock


<|start_filename|>src/widgets/DeviceWidget.css<|end_filename|>
.Unsupported {
  display: flex;
  height: 100%;
  box-sizing: border-box;
  flex-direction: column;
  background: repeating-linear-gradient(
    -45deg,
    #ffc46c,
    #ffc46c 20px,
    transparent 20px,
    transparent 40px
  );
  padding: 5px;
}

.Unsupported h2 {
  padding: 5px;
  margin: 0;
  font-size: 1em;
  font-weight: lighter;
  text-align: center;
  white-space: nowrap;
  box-sizing: border-box;
  pointer-events: none;
  text-overflow: ellipsis;
  overflow: hidden;
  z-index: 1;
}

.Unsupported textarea {
  resize: none;
  flex: 1;
}


<|start_filename|>src/Themes.js<|end_filename|>
const Themes = {
  'Default' : {
    appBackground: 'white',
    appText: '#3a3a3a',
    background: 'white',
    buttonMixed: '#4a4a4a',
    buttonOff: '#8a8a8a',
    buttonOn: '#ffbc00',
    menuIcon: '#666',
    menuIconSelected: '#ffbc00',
    overlayBackground: 'rgba(255, 255, 255, .85)',
    overlayText: '#666',
    text: '#666',
    textMixed: 'white',
    textOff: 'white',
    textOn: '#3E2723',
    unlockedBackground: '#eeeeee'
  },
  'Lighter' : {
    appBackground: 'white',
    appText: '#3a3a3a',
    background: 'white',
    buttonMixed: '#808080',
    buttonOff: '#eee',
    buttonOn: '#ffbc00',
    menuIcon: '#666',
    menuIconSelected: '#ffbc00',
    overlayBackground: 'rgba(255, 255, 255, .85)',
    overlayText: '#666',
    text: '#808080',
    textMixed: '#808080',
    textOff: '#808080',
    textOn: 'white',
    unlockedBackground: '#eeeeee'
  },
  'Light Blue' : {
    appBackground: 'white',
    appText: 'black',
    background: '#99CCFF',
    buttonMixed: '#4a4a4a',
    buttonOff: '#8a8a8a',
    buttonOn: '#3366FF',
    menuIcon: '#666',
    menuIconSelected: '#3366FF',
    overlayBackground: 'rgba(255, 255, 255, .85)',
    overlayText: '#666',
    text: 'black',
    textMixed: 'white',
    textOff: 'white',
    textOn: 'white',
    unlockedBackground: '#eeeeee'
  },
  'Light Green' : {
    appBackground: 'white',
    appText: 'black',
    background: '#66FF99',
    buttonMixed: '#4a4a4a',
    buttonOff: '#8a8a8a',
    buttonOn: '#009900',
    menuIcon: '#666',
    menuIconSelected: '#009900',
    overlayBackground: 'rgba(255, 255, 255, .85)',
    overlayText: '#666',
    text: 'black',
    textMixed: 'white',
    textOff: 'white',
    textOn: 'white',
    unlockedBackground: '#eeeeee'
  },
  'Dark Grey' : {
    appBackground: '#303030',
    appText: '#F5F5F5',
    background: '#424242',
    buttonMixed: '#607D8B',
    buttonOff: '#616161',
    buttonOn: '#607D8B',
    menuIcon: '#607D8B',
    menuIconSelected: '#F5F5F5',
    overlayBackground: 'rgba(66, 66, 66, 0.85)',
    overlayText: '#90A4AE',
    text: '#F5F5F5',
    textMixed: '#F5F5F5',
    textOff: '#F5F5F5',
    textOn: '#F5F5F5',
    unlockedBackground: 'repeating-linear-gradient(45deg, #607D8B, #607D8B 5px, #90A4AE 10px, #90A4AE 20px)'
  },
  'Dark' : {
    appBackground: '#636363',
    appText: '#dadada',
    background: 'rgba(37, 37, 37, .1)',
    buttonMixed: 'rgba(37, 37, 37, .9)',
    buttonOff: 'rgba(37, 37, 37, .4)',
    buttonOn: 'rgba(37, 37, 37, .9)',
    menuIcon: 'rgba(33, 33, 33, .9)',
    menuIconSelected: '#dadada',
    overlayBackground: 'rgba(68, 68, 68, .8)',
    overlayText: '#dadada',
    text: '#dadada',
    textMixed: '#dadada',
    textOff: '#dadada',
    textOn: '#dadada',
    unlockedBackground: 'rgba(51, 51, 51, .9)'
  }
}
export default Themes


<|start_filename|>src/SettingsView.css<|end_filename|>
.SettingsView {
  margin: 50px 20px;
}

.Status {
  color: red;
  font-weight: normal;
}

.Status.OK {
  color: green;
}

.SettingsView fieldset {
  max-width: 480px;
  padding: 5px 10px;
  margin-top: 5px;
  border: 1px solid #d3d3d3;
}

.SettingsView fieldset label {
  display: flex;
  align-items: center
}

.SettingsView input[type=text], input[type=password], input[type=url]  {
  display: block;
  width: 100%;
  max-width: 500px;
  font-size: .9em;
  margin: 5px 0;
}

.SettingsView input[type=submit], .SettingsView button {
  font-size: 1.12em;
  margin-left: 0;
  color: inherit;
  background: rgba(211, 211, 211, .5);
  border: 1px solid #d3d3d3;
}

.SettingsView em {
  font-size: .9em;
  color: #666;
}


<|start_filename|>src/widgets/SwitchLock.css<|end_filename|>
button.SwitchLock {
  display: flex;
  align-items: center;
}

.SwitchLock > * {
  flex: 2;
  margin: 0 5px;
  overflow: hidden;
}

.SwitchLock .lockicon {
  height: 70%;
  min-width: 35px;
  flex: 1;
  opacity: 1;
  transform: scaleX(-1);
}



<|start_filename|>src/widgets/SwitchSelector.css<|end_filename|>
.selectorContainer {
  height: 100%;
}

.selector {
  display: flex;
  height: 100%;
  flex-direction: column;
}

.selector h2 {
  width: 100%;
  padding: 10px;
  margin: 0;
  font-size: 1em;
  font-weight: lighter;
  text-align: center;
  white-space: nowrap;
  box-sizing: border-box;
  pointer-events: none;
  text-overflow: ellipsis;
  overflow: hidden;
  z-index: 1;
}

.selector > div {
  position: relative;
  flex: 1;
}

.selector > section {
  flex: 1;
  display: flex;
  flex-direction: row;
}

.selector button {
  flex: 1;
  margin: 0 5px;
  height: auto;
}

@media (max-width: 414px) {
	.selector button {
		font-size: 1em;
	}
}

.selector button:first-child {
  margin-left: 0;
}

.selector button:last-child {
  margin-right: 0;
}

.selector .selectContainer {
  display: flex;
}

.selector select {
  width: 100%;
  padding: 0 30px 0 10px;
  font-size: 1.25em;
  font-weight: 100;
  text-align-last: center;
  border: none;
  border-radius: 0;
  outline: none;
  transition: color .2s ease-out, background .2s ease-out;
  -webkit-appearance: none;
  -moz-appearance: none;
}

.selector select[disabled] {
  pointer-events: none;
}

.carret {
  font-style: normal;
  position: absolute;
  right: 3%;
  padding-right: 5px;
  top: calc(50% - 6px);
  pointer-events: none;
  transition: color .2s ease-out;
}

.selectorContainer h2 {
  position: absolute;
  text-align: center;
  width: 100%;
  pointer-events: none;
}


<|start_filename|>src/widgets/WeatherInfoWidget.js<|end_filename|>
import React, { Component } from 'react';
import GenericWidget from './helpers/GenericWidget';
import './WeatherInfoWidget.css';

class WeatherInfoWidget extends Component {

  render() {
    if (this.props.visibility !== undefined) {
      return (
      <GenericWidget class="WeatherInfoWidget" icon="eye"
        isOn={this.props.visibility > 0}
        value1={this.props.visibility}
        value1Class="visibility"
        {...this.props} />
      );
    }
    if (this.props.radiation !== undefined) {
      return (
      <GenericWidget class="WeatherInfoWidget" icon="weather-sunny"
        isOn={this.props.radiation > 0}
        value1={this.props.radiation}
        value1Class="radiation"
        {...this.props} />
      );
    }
    return (
    <GenericWidget class="WeatherInfoWidget" icon="sunglasses"
      isOn={this.props.uv > 0}
      value1={this.props.uv}
      value1Class="uv"
      {...this.props} />
    );
  }
}

export default WeatherInfoWidget


<|start_filename|>src/widgets/PercentWidget.js<|end_filename|>
import React, { Component } from 'react';
import GenericWidget from './helpers/GenericWidget';
import './PercentWidget.css';

class PercentWidget extends Component {

  constructor(props) {
    super(props);
    this.radius = 70;
  }

  render() {
    const dashArray = Math.PI * this.radius * 2;
    const dashOffset = ((100 - Number(this.props.value)) / 100) * Math.PI *
       (this.radius * 2);
    const theme = this.props.theme;
    const circleStyle = theme ? {
      stroke: theme.buttonOff,
      strokeDasharray: dashArray
    } : {};
    const barStyle = theme ? {
      stroke: theme.buttonOn,
      strokeDasharray: dashArray,
      strokeDashoffset: dashOffset
    } : {};
    return (
      <GenericWidget class='PercentWidget'
          value1={Number(this.props.value).toFixed(0)}
          {...this.props}>
        {this.props.layoutWidth > 1 &&
            <svg className="doughnut" viewBox="0 0 200 200" version="1.1" xmlns="http://www.w3.org/2000/svg">
              <circle style={circleStyle} r={this.radius} cx="100" cy="100"></circle>
              <circle className="bar" style={barStyle} r={this.radius} cx="100" cy="100"></circle>
            </svg>}
      </GenericWidget>
    );
  }

}

export default PercentWidget


<|start_filename|>src/DeviceListView.test.js<|end_filename|>
import React from 'react';
import ReactDOM from 'react-dom';
import { mount } from 'enzyme';
import DeviceListView from './DeviceListView';

it('renders without crashing', () => {
  const wrapper = mount(<DeviceListView />);
});


<|start_filename|>src/widgets/SwitchDimmer.css<|end_filename|>
.SwitchDimmer {
  display: flex;
  height: 100%;
}

.SwitchDimmer .toggle {
  width: 15%;
  height: 100%;
  min-width: 40px;
  border: none;
  padding: 5px;
}

.SwitchDimmer .toggle:only-child {
  width: 100%;
  border: none;
}

.SwitchDimmer button {
  display: block;
  margin: 0;
  transition: all .3s ease-out;
  font-size: 1.12em;
  font-weight: lighter;
  text-overflow: ellipsis;
  overflow: hidden;
}

.SwitchDimmer button:active {
  transform: scale(0.9);
  outline: none;
}

.SwitchDimmer button:focus {
  outline: none;
}

.SwitchDimmer .dimmerSlider {
  position: relative;
  width: 100%;
  margin-left: 10px;
}

.SwitchDimmer .dimmerContainer, .dimmerProgress, .dimmerProgressContent {
  position: absolute;
  top: 0;
  left: 0;
  margin: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  transition: transform .3s ease-out;
  pointer-events: none;
  user-select: none;
}

.SwitchDimmer .dimmerProgress {
  margin-left: -100%;
}

.SwitchDimmer .dimmerContainer, .dimmerProgressContent {
  display: flex;
  justify-content: center;
  align-items: center;
}

.SwitchDimmer .dimmerContainer {
  font-size: 1.25em;
  font-weight: lighter;
  text-align: center;
}

.SwitchDimmer .dimmerProgressContent {
  position: relative;
  color: white;
}

.SwitchDimmer .dimmerContainer p {
  font-weight: lighter;
}


<|start_filename|>src/RoomplanSelector.js<|end_filename|>
import React, { Component } from 'react';
import JSONClientSingleton from './util/JSONClientSingleton'

class RoomplanSelector extends Component {

  constructor(props) {
    super(props);
    this.state = {
      names: [],
      idx: [],
      sceneIds: []
    };
    this.json = new JSONClientSingleton();
  }

  componentDidMount() {
    this.json.get({
      type: 'plans',
      order: 'name',
      used: 'true'
    }, (data) => {
      if (data.status !== "OK") {
        alert("Unable to get floor plans.");
        return;
      }
      if (!data.result) {
        return;
      }
      const names = [];
      const idx = [];
      for (let i = 0 ; i < data.result.length; i++) {
        names.push(data.result[i]['Name']);
        idx.push(data.result[i]['idx']);
      }
      this.setState({names: names, idx: idx});
    });
    this.json.getAllScenes((data) => {
      if (data.status !== "OK") {
        alert("Unable to get scenes status.");
        return;
      }
      if (!data.result) {
        return;
      }
      const scenes = data.result;
      const sceneIds = [];
      for (let i = 0; i < scenes.length; i++) {
        const scene = scenes[i];
        const fullId = (scene.Type === 'Group' ? 'g' : 's') + '|' + scene.idx;
        sceneIds.push(fullId);
      }
      this.setState({sceneIds: sceneIds});
    });
  }

  getUid(device) {
    if (device.type === 1) {
      const sceneId = 's|' + device.devidx;
      if (this.state.sceneIds.indexOf(sceneId) >= 0) {
        return sceneId;
      }
      const groupId = 'g|' + device.devidx;
      if (this.state.sceneIds.indexOf(groupId) >= 0) {
        return groupId;
      }
      alert('Unknown scene id: ' + device.devidx);
    }
    return 'd|' + device.devidx;
  }

  handleSelect = (event) => {
    if (event.target.value === "none") {
      return;
    }
    if (this.props.needConfirm && !window.confirm(
        'Replace current selection with the devices of this room ?')) {
      return;
    }
    // Force-reset layout.
    this.props.onWhitelistChange([]);
    const message = {
      type: 'command',
      param: 'getplandevices',
      idx: event.target.value
    };
    this.json.get(message, (data) => {
      const whitelist = [];
      if (data.result) {
        for (let i = 0 ; i < data.result.length; i++) {
          whitelist.push(this.getUid(data.result[i]));
        }
      }
      this.props.onWhitelistChange(whitelist);
    });
  }

  render() {
    const options = [];
    if (this.state.idx.length) {
      options.push(<option key='nofp' value='none'>select by room plan</option>);
      for (let i = 0; i < this.state.idx.length; i++) {
        const id = this.state.idx[i];
        options.push(
          <option key={'fp' + id} value={id}>{this.state.names[i]}</option>
        );
      };
    }
    return (
      <span>
        {options.length ? <span>, or <select value="none" onChange={this.handleSelect}>{options}</select></span> : ''}
      </span>
    );
  }

}

export default RoomplanSelector


<|start_filename|>src/util/LocalStorage.js<|end_filename|>

class LocalStorage {
  constructor() {
    let hasLocalStorage = 'localStorage' in global;
    this.ls = null;
    if (hasLocalStorage) {
      const testKey = 'localstorage.test-key';
      try {
        this.ls = global.localStorage;
        this.ls.setItem(testKey, 'foo');
        this.ls.removeItem(testKey);
      } catch (e) {
        hasLocalStorage = false;
      }
    } else {
      console.log('Localstorage not supported !');
    }
  }

  write(key, data) {
    this.ls && this.ls.setItem(key, JSON.stringify(data));
  }

  read(key) {
    if (this.ls) {
      return JSON.parse(this.ls.getItem(key));
    }
    return null;
  }

  getKeys() {
    if (this.ls) {
      return Object.keys(this.ls);
    }
    return [];
  }

  delete(key) {
    this.ls && this.ls.removeItem(key);
  }

}

export default LocalStorage;


<|start_filename|>src/widgets/GasWidget.css<|end_filename|>
.GasWidget .value1::after {
  content: ' m3';
}


<|start_filename|>src/SettingsView.js<|end_filename|>
import React, { Component } from 'react';
import './SettingsView.css';
import icon from './ext/icon_64.png';

class SettingsView extends Component {

  constructor(props) {
    super(props);
    this.state = {
      domoticzAuthChecked: props.config ?
          !!props.config.domoticzAuthChecked : false,
      domoticzLogin: props.config ? props.config.domoticzLogin : '',
      domoticzPassword: props.config ? props.config.domoticzPassword : '',
      domoticzUrl: props.config ? props.config.domoticzUrl : '',
      mqttAuthChecked: props.config ? !!props.config.mqttAuthChecked : false,
      mqttBrokerUrl: props.config ? props.config.mqttBrokerUrl : '',
      mqttLogin: props.config ? props.config.mqttLogin : '',
      mqttPassword: props.config ? props.config.mqttPassword : ''
    };
  }

  handleChange = event => {
    let target = event.target;
    const newPartialState = {};
    newPartialState[target.name] = target.type === "checkbox" ?
        target.checked : target.value;
    this.setState(newPartialState);
  }

  handleSubmit = event => {
    event.preventDefault();
    const state = Object.assign({}, this.state);
    if (!state.mqttAuthChecked) {
      state.mqttLogin = '';
      state.mqttPassword = '';
    }
    if (!state.domoticzAuthChecked) {
      state.domoticzLogin = '';
      state.domoticzPassword = '';
    }
    this.setState(state);
    this.props.onChange(state);
  }

  renderWelcomeMsg = () => {
    return(
      <div>
        <h1>Reacticz</h1>
        <img src={icon} alt="Reacticz logo"/>
        <p>Welcome to Reacticz, the minimalistic <a href="http://www.domoticz.com" target="_blank" rel="noopener noreferrer">Domoticz</a> dashboard!<br/>
        Documentation is available on  <a href="https://github.com/domoticz/Reacticz#reacticz" target="_blank" rel="noopener noreferrer">GitHub</a>.</p>
        <p>Please setup your server config to proceed.</p>
        <p><em>Not your first time here?&nbsp;&nbsp;<button onClick={this.props.importConfigPrompt}>Import config from another dashboard</button></em></p>
      </div>
    );
  }

  renderMqttAuthForm = () => {
    return(
      <fieldset>
        <label>
          Username&nbsp;
          <input type="text" value={this.state.mqttLogin} name="mqttLogin" placeholder="Username" onChange={this.handleChange} />
        </label>
        <label>
          Password&nbsp;
          <input type="password" value={this.state.mqttPassword} name="mqttPassword" placeholder="Password" onChange={this.handleChange} />
        </label>
      </fieldset>
    );
  }

  renderDomoticzAuthForm = () => {
    return(
      <fieldset>
        <label>
          Username&nbsp;
          <input type="text" value={this.state.domoticzLogin} name="domoticzLogin" placeholder="Username" onChange={this.handleChange} />
        </label>
        <label>
          Password&nbsp;
          <input type="password" value={this.state.domoticzPassword} name="domoticzPassword" placeholder="Password" onChange={this.handleChange} />
        </label>
      </fieldset>
    );
  }

  render() {
    const mqttOk = this.props.serverStatus && this.props.serverStatus.mqtt ? <span className="Status OK">connected!</span> : <span className="Status">unavailable</span>;
    const domoticzOk = this.props.serverStatus && this.props.serverStatus.domoticz ? <span className="Status OK">connected!</span> : <span className="Status">unavailable</span>;

    return (
      <div className="SettingsView">
        {!(this.props.config && this.props.config.mqttBrokerUrl && this.props.config.domoticzUrl) && this.renderWelcomeMsg()}
        <h2>Server Settings</h2>
        <p>To use Reacticz you need a working Domoticz server and a configured MQTT broker with websockets enabled (see <a href="https://github.com/domoticz/Reacticz#requirements" target="_blank" rel="noopener noreferrer">requirements</a>).</p>
        <form onSubmit={this.handleSubmit}>
          <h3>MQTT {this.props.config && this.props.config.mqttBrokerUrl && mqttOk}</h3>
          <label>
            Broker URL:
            <input type="url" value={this.state.mqttBrokerUrl} name="mqttBrokerUrl" placeholder="ws://mqtt-broker:port" onChange={this.handleChange} />
          </label>
          <label>
            <input type="checkbox" name="mqttAuthChecked" onChange={this.handleChange}
                checked={this.state.mqttAuthChecked} /> use authentication
          </label>
          <br/>
          {this.state.mqttAuthChecked && this.renderMqttAuthForm()}
          <br/>
          <h3>Domoticz {this.props.config && this.props.config.domoticzUrl && domoticzOk}</h3>
          <label>
            Server URL:
            <input type="url" value={this.state.domoticzUrl} name="domoticzUrl" placeholder="http://domoticz-server:port" onChange={this.handleChange} />
          </label>
          <label>
            <input type="checkbox" name="domoticzAuthChecked" onChange={this.handleChange}
                checked={this.state.domoticzAuthChecked} /> use authentication
          </label>
          <br/>
          {this.state.domoticzAuthChecked && this.renderDomoticzAuthForm()}
          <br /><input type="submit" value="Apply &amp; Save" />
        </form>
      </div>
    );
  }
}

export default SettingsView;


<|start_filename|>src/widgets/GasWidget.js<|end_filename|>
import React, { Component } from 'react';
import GenericWidget from './helpers/GenericWidget';
import './GasWidget.css';

class GasWidget extends Component {

  render() {
    return (
      <GenericWidget class="GasWidget"
          icon="whatshot"
          isOn={Number(this.props.usage) > 0 }
          value1={Number(this.props.usage).toFixed(0) / 1000}
          {...this.props} />
    );
  }

}

export default GasWidget


<|start_filename|>src/widgets/RainWidget.js<|end_filename|>
import React, { Component } from 'react';
import GenericWidget from './helpers/GenericWidget';
import './RainWidget.css';

class RainWidget extends Component {

  render() {
    return (
      <GenericWidget class="RainWidget" icon="weather-rainy"
          isOn={this.props.rain > 0}
          value1={this.props.rain}
          value2={this.props.rate}
          {...this.props} />
    );
  }

}

export default RainWidget


<|start_filename|>src/widgets/AlertWidget.css<|end_filename|>
.AlertWidget.level0 {
  fill: gray;
}

.AlertWidget.level1 {
  fill: green;
}

.AlertWidget.level2 {
  fill: #ffe01c;
}

.AlertWidget.level3 {
  fill: orange;
}

.AlertWidget.level4 {
  fill: #e80a0a;
}



<|start_filename|>src/DeviceListView.css<|end_filename|>
.DeviceListView {
  margin: 50px 20px 60px 20px;
}

.DeviceListView ul {
  padding-left: 10px;
}

.DeviceListView li {
  list-style-type: none;
  margin: 5px 0;
}

.DeviceListView li.unused {
  text-decoration: line-through;
}

.DeviceListView input {
  margin-right: 5px;
}

.DeviceListView label {
  cursor: pointer;
}

.searchbar {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  margin-top: 20px;
}

.searchbar > div {
  white-space: nowrap;
  margin-right: 20px;
}


<|start_filename|>src/widgets/helpers/Slider.css<|end_filename|>
.Slider, .Slider .bar {
  position: absolute;
  top: 0;
  left: 0;
  margin: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  pointer-events: all;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
}

.Slider .bar {
  margin-left: -100%;
  box-sizing: border-box;
  border-right: 3px solid #4a4a4a;
  transition: transform .3s ease-out, opacity .3s linear;
  pointer-events: none;
}


<|start_filename|>src/widgets/WeatherWidget.js<|end_filename|>
/*
svalue1 = temp
svalue2 = humidity
svalue3 = hum stat (0=Normal, 1=Comfortable, 2=Dry, 3=Wet)
svalue4 = baro hPa
svalue5 = forecast (0 = No info, 1 = Sunny, 2 = Partly cloudy, 3 = Cloudy, 4 = Rain)
*/

import React, { Component } from 'react';

import './WeatherWidget.css';

class WeatherWidget extends Component {

  getProperties() {
    const device = this.props.device;
    const temp = Number(device.svalue1).toFixed(1);
    switch (device.dtype) {
      case "Temp + Humidity + Baro" :
        return { temp: temp, hum: device.svalue2, baro: this.props.hidePressure ? undefined : device.svalue4, forecast: device.svalue5 };
      default:}
  }

  render() {
    const params = this.getProperties();
    const theme = this.props.theme;
    const style = theme ? {
      background: this.props.readOnly ? theme.unlockedBackground : theme.background,
      color: theme.text
    } : {};
    return (
      <div className="WeatherWidget" style={style}>
        {params.forecast !== undefined && this.props.layoutWidth > 1 && <div className={'weatherImage weather' + params.forecast}></div>}
        <div className={'weatherData'}>
          <div className="name">{this.props.device.name}</div>
          {params.temp !== undefined && <div className="temp">{params.temp}</div>}
          {params.baro !== undefined && <div className="baro">{params.baro}</div>}
          {params.hum !== undefined && <div className="hum">{params.hum}</div>}
        </div>
      </div>
    );
  }

}

export default WeatherWidget


<|start_filename|>src/widgets/ThermostatWidget.css<|end_filename|>
.ThermostatWidget {
  display: flex;
  flex-direction: column;
  height: 100%;
}

.ThermostatWidget .controls {
  display: flex;
  margin: 0;
  align-items: center;
  flex: 1;
}

.ThermostatWidget h2 {
  width: 100%;
  padding: 10px;
  margin: 0;
  font-size: 1em;
  font-weight: lighter;
  text-align: center;
  white-space: nowrap;
  box-sizing: border-box;
  pointer-events: none;
  text-overflow: ellipsis;
  overflow: hidden;
  z-index: 1;
}

.ThermostatWidget .controls div,
.ThermostatWidget button {
  padding: 10px;
  flex: 1;
  box-sizing: border-box;
}

.ThermostatWidget .controls div {
  flex: 2;
  font-size: 1.75em;
  font-weight: lighter;
  text-align: center;
  text-overflow: ellipsis;
  overflow: hidden;
}

.ThermostatWidget .controls div::after {
  content: '°';
}

.ThermostatWidget button {
  font-size: 1.75em;
  line-height: 1.75em;
}

@-webkit-keyframes blinker {
  15%, 40% {opacity: 0;}
  0%, 55%, 100% {opacity: 1;}
}

.ThermostatWidget .blink{
  text-decoration: blink;
  -webkit-animation-name: blinker;
  -webkit-animation-duration: .5s;
  -webkit-animation-iteration-count:infinite;
  -webkit-animation-timing-function:ease-in-out;
}



<|start_filename|>src/App.css<|end_filename|>
/*  Main css file */

body {
  -webkit-tap-highlight-color: rgba(0,0,0,0);
}

a {
  color: inherit;
  filter: invert(30%);
}

.gridItem {
  overflow: hidden;
}

.gridItem.resizeable {
  animation-duration: .3s;
  animation-name: bgcolor;
  animation-timing-function: ease-out;
  animation-fill-mode: forwards;
}

@keyframes bgcolor {
  from {
    background-color: transparent;
  }
  to {
    background-color: #eee;
  }
}

.appbar {
  top: 0;
  right: 0;
  position: fixed;
  background-color: transparent;
  transform: translate3d(calc(100% - 52px), 0, 0);
  transition: transform .3s ease-in-out, background-color .3s linear;
  z-index: 2;
  will-change: transform;
}

.icon {
  width: 34px;
  height: 34px;
  vertical-align: middle;
}

.appbar button {
  background: transparent;
  border: none;
  cursor: pointer;
  margin: 4px;
  outline: none;
}

.appbar.open {
  transform: translate3d(0, 0, 0);
}

.appbar button:first-child {
  transition: transform .3s ease-in-out, color .3s linear;
}

.appbar.open button:first-child {
  transform: rotate(-120deg);
}

.addDevices {
  margin: 50px 20px;
}

.addDevices i {
  color: #666;
  vertical-align: middle;
}

.addDevices .icon {
  width: 24px;
  height: 24px;
}

.layout {
  margin-bottom: 50px;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
}

.layout button {
  border-radius: 0;
}

.footer {
  position:fixed;
  left:0;
  bottom:0;
  width: 100%;
  z-index: 2;
  transition: transform .3s ease-out;
  will-change: transform;
  -webkit-user-select: none;
}

.footer.hidden {
  transform: translate3d(0, 50px, 0);
}

.footer .left {
  float: left;
}

.footer .title {
  text-align: center;
}

.footer .right {
  float: right;
}

.footer button {
  background: transparent;
  border: none;
  cursor: pointer;
  margin: 4px;
  outline: none;
}

.zoomlabel {
    font-style: italic;
    font-size: small;
    padding: 2px;
    text-align: center;
}


<|start_filename|>src/widgets/RainWidget.css<|end_filename|>
.RainWidget .value1::after {
  content: ' mm';
}

.RainWidget .value2::after {
  content: ' mm/h';
}


<|start_filename|>src/widgets/ContactWidget.js<|end_filename|>
import React, { Component } from 'react';
import GenericWidget from './helpers/GenericWidget';

class ContactWidget extends Component {

  render() {
    return (
      <GenericWidget icon={this.props.value === 0 ? 'closed' : 'open' }
          isOn={Number(this.props.value) > 0}
          value1={this.props.value === 0 ? 'Closed' : 'Open'}
          {...this.props} />
    );
  }

}

export default ContactWidget


<|start_filename|>src/widgets/WindWidget.css<|end_filename|>
.WindWidget .value2::after {
  content: ' km/h';
}

.WindWidget svg {
  transition: transform .3s ease-out;
}


<|start_filename|>src/App.js<|end_filename|>
import React, { Component } from 'react';
import {Responsive, WidthProvider} from 'react-grid-layout';
import AboutView from './AboutView';
import ConfigStorageHelper from './util/ConfigStorageHelper';
import DeviceListView from './DeviceListView';
import DeviceWidget from './widgets/DeviceWidget';
import JSONClientSingleton from './util/JSONClientSingleton';
import LZString from 'lz-string';
import LoadingWidget from './widgets/LoadingWidget';
import LocalStorage from './util/LocalStorage';
import MqttClientSingleton from './util/MqttClientSingleton';
import SceneWidget from './widgets/SceneWidget';
import Screensaver from './Screensaver';
import SettingsView from './SettingsView';
import Themes from './Themes';
import './App.css';
import '../node_modules/react-grid-layout/css/styles.css';
import '../node_modules/react-resizable/css/styles.css';

const ResponsiveGridLayout = WidthProvider(Responsive);

const MAX_SCREENSAVER_DELAY_SEC = 3600;
const DELAY_OFF_VALUE = -1;

const View = {
  DASHBOARD: 1,
  DEVICE_LIST: 2,
  SERVER_SETTINGS: 3,
  ABOUT: 4
}

class App extends Component {

  constructor(props) {
    super(props);
    this.state = {
      menuOpen: false,
      currentView: View.DASHBOARD,
      layoutLocked: true,
      serverConfig: {
        mqttBrokerUrl: '',
        mqttLogin: '',
        mqttPassword: '',
        domoticzUrl: '',
        domoticzLogin: '',
        domoticzPassword: ''
      },
      serverStatus: {
        mqtt: false,
        domoticz: false,
      },
      lastMqttMessage: null,
      configId: 0,
      configName: '',
      multiConfig: false,
      devices: {},
      deviceSpecs: {},
      layout: [],
      whitelist: [],
      zoom: 1,
      screensaverConfig: {
        delay: -1,
        type: "blank"
      },
      themeId: 'Default',
      theme: {}
    };
    this.json = new JSONClientSingleton();
    this.json.setEventHandler(this.domoticzEventHandler);
    this.mqtt = new MqttClientSingleton();
    this.mqtt.setEventHandler(this.mqttEventHandler);
    this.store = new LocalStorage();
    this.configHelper = new ConfigStorageHelper();
  }

  componentWillMount() {
    const storedServerConfig = this.store.read('serverConfig');
    const themeId = this.store.read('themeId') || this.state.themeId;
    const zoom = this.store.read('zoom') || this.state.zoom;
    const screensaverConfig = this.store.read('screensaverConfig') || this.state.screensaverConfig;
    const configId = this.readConfigIdFromUrlParam();
    const configs = this.configHelper.getConfigs();
    const config = this.configHelper.getConfig(configId) || {};
    const configName = config.name || 'Dashboard';
    this.setState({
      whitelist: config.whitelist || [],
      layout: config.layout || [],
      configId: configId,
      configName: configName,
      multiConfig: configs.length > 1,
      zoom: zoom,
      screensaverConfig: screensaverConfig,
      themeId: themeId,
      theme: Themes[themeId] || {}
    });
    if (storedServerConfig) {
      this.setState({serverConfig: storedServerConfig});
    }
    this.setState({ multiConfig: this.configHelper.getConfigs().length > 1});
    window.onpopstate = this.handlePopstate;
    this.updateDocumentTitle(configName);
  }

  componentDidMount() {
    this.connectClients(this.state.serverConfig);
    this.readSharedConfigFromUrl();
    if (this.hasWhitelistedScenes()) {
      this.requestScenesStatus();
    }
    // store intervalId in the state so it can be accessed later:
    this.setState({intervalId: setInterval(this.timer, 5000)});
  }

  componentWillUnmount() {
    this.json.setEventHandler(() => {});
    this.mqtt.setEventHandler(() => {});
    // use intervalId from the state to clear the interval
    clearInterval(this.state.intervalId);
  }

  timer=()=>{
    this.state.whitelist.forEach(device => this.requestDeviceStatus(device));
  }

  handlePopstate = (event) => {
    if (event.state && event.state.id !== undefined) {
      this.loadConfig(event.state.id, true /* opt_noHistoryPush */ );
    }
  }

  connectClients(config) {
    this.mqtt.connect(config.mqttBrokerUrl, config.mqttLogin, config.mqttPassword);
    this.json.setServerUrl(config.domoticzUrl, config.domoticzLogin, config.domoticzPassword);
  }

  mqttEventHandler = (eventType, opt_data = null) => {
    switch (eventType) {
      case 'connected':
        this.setState({serverStatus:
            Object.assign(this.state.serverStatus, {mqtt: !!opt_data})});
        if (opt_data === true) {
          for (let i = 0; i < this.state.whitelist.length; i++) {
            this.requestDeviceStatus(this.state.whitelist[i]);
          }
        }
        break;
      case 'error':
        // nothing to do here for now.
        break;
      case 'message':
        if (this.state.whitelist.indexOf('d|' + opt_data.idx) >= 0) {
          const devices = Object.assign({}, this.state.devices);
          devices['d|' + opt_data.idx] = opt_data;
          this.setState({devices: devices});
          // Update scene status (with a little debounce check).
          if (this.hasWhitelistedScenes()) {
            this.requestScenesStatus(true /* opt_throttle */);
          }
        }
        this.render();
        break;
      default:
        console.log('unknown event type from MqttClientSingleton', eventType);
        break;
    }
  }

  domoticzEventHandler = (eventType, opt_data = null) => {
    switch (eventType) {
      case 'connected':
        this.setState({serverStatus:
            Object.assign(this.state.serverStatus, {domoticz: !!opt_data})});
        break;
      default:
        console.log('unknown event type from JSONClientSingleton', eventType);
        break;
    }
  }

  updateDocumentTitle = (configName) => {
    if (document) {
      document.title = configName ? configName + ' | Reacticz' : 'Reacticz';
    }
  }

  readConfigIdFromUrlParam() {
    const param = Number(document.location.search.slice(1));
    if (param >= 0 && Math.round(param) === param) {
      if (this.configHelper.getConfig(param)) {
        return param;
      }
      return 0;
    }
    return 0;
  }

  getPrevConfig = () => {
    this.getNewConfig(true /* opt_prev */);
  }

  getNextConfig = () => {
    this.getNewConfig();
  }

  getNewConfig = (opt_prev = false) => {
    const newId = this.configHelper.getNextId(this.state.configId, opt_prev);
    this.loadConfig(newId);
  }

  loadConfig(id, opt_noHistoryPush = false) {
    const config = this.configHelper.getConfig(id);
    if (!config) {
      console.log('Unable to load config with id ' + id);
      return;
    }
    this.setState({
      configId: id,
      configName: config.name,
      layout: config.layout,
      whitelist: config.whitelist
    });
    !opt_noHistoryPush && window.history.pushState({id: id}, '', '?' + id);
    this.setState({
      multiConfig: this.configHelper.getConfigs().length > 1,
      layoutLocked: true
    });
    this.updateDocumentTitle(config.name);
    for (let i = 0; i < config.whitelist.length; i++) {
      this.requestDeviceStatus(config.whitelist[i]);
    }
  }

  deleteCurrentConfig = () => {
    if (!window.confirm('Delete this dashboard ?')) {
      return;
    }
    this.configHelper.deleteConfig(this.state.configId);
    const nextConfigId =
        this.configHelper.getConfigs().length >= this.state.configId + 1 ?
        this.state.configId : 0;
    this.loadConfig(nextConfigId);
  }

  readSharedConfigFromUrl = (opt_param) => {
    const param = opt_param || document.location.hash.slice(1);
    if (!param) {
      return;
    }
    if (window.confirm(
      'Reacticz configuration parameters detected! Apply here?\n' +
      '\nNote: if a dashboard configuration is found in the parameters and ' +
      'your already have one setup here, a new separate dashboard will be ' +
      'created.')) {
      try {
        const config =
            JSON.parse(LZString.decompressFromEncodedURIComponent(param));
        config.s && this.handleServerConfigChange(config.s);
        config.t && this.handleThemeChange(config.t);
        if (config.w) {
          // Importing dashboard config.
          if (this.state.whitelist.length > 0) {
            console.log('adding config');
            // Don't override the current dahboard.
            this.loadConfig(this.configHelper.addConfig({
              name: config.n || 'Imported dashboard',
              layout: config.l || [],
              whitelist: config.w || []
            }));
            return;
          }
          this.handleConfigNameChange(config.n || 'Imported dashboard');
          this.handleDeviceListChange(config.w || []);
          this.storeConfigChange({layout: config.l || []});
          this.setState({layout: config.l || []});
        }
      } catch (e) {
        console.log(e);
        alert('Sorry, something went wrong. The configuration could not be read.');
      }
    }
    document.location.hash = '';
  }

  importConfigPrompt = () => {
    const url = prompt('Please paste the full configuration URL to import.\n\n'
        + 'This URL can be found in the About page of a configured Reacticz '
        + 'dashboard.');
    if (url && url.split('#').length === 2) {
      this.readSharedConfigFromUrl(url.split('#')[1]);
    }
  }

  handleServerConfigChange = (config) => {
    this.setState({serverConfig: config});
    this.store.write('serverConfig', config);
    this.connectClients(config);
  }

  handleThemeChange = (themeId) => {
    const theme = Themes[themeId];
    if (theme) {
      this.setState({themeId: themeId, theme: theme});
      this.store.write('themeId', themeId);
    }
  }

  handleZoomChange = (zoom) => {
    this.setState({zoom: zoom});
    this.store.write('zoom', zoom);
  }

  handleScreensaverConfigChange = (config = {}) => {
    config.delay = config.delay !== DELAY_OFF_VALUE ? Math.abs(Math.min(config.delay, MAX_SCREENSAVER_DELAY_SEC)) : config.delay;
    this.setState({
      screensaverConfig: config
    });
    this.store.write('screensaverConfig', config);
  }

  handleConfigNameChange = (configName) => {
    this.setState({configName: configName});
    this.storeConfigChange({name: configName});
  }

  storeConfigChange = (partialConfigObject) => {
    this.configHelper.storeConfigChange(
        this.state.configId, partialConfigObject);
  }

  handleDeviceListChange = (list) => {
    const devices = Object.assign({}, this.state.devices);
    // Delete device info for devices that were removed from the whitelist.
    for (let i = 0; i < this.state.whitelist.length; i++) {
      const id = this.state.whitelist[i];
      if (list.indexOf(id) < 0) {
        delete devices[id];
      }
    }
    this.setState({whitelist: list, devices: devices});
    this.storeConfigChange({whitelist: list})
    this.cleanupLayout(list);
    for (let i = 0; i < list.length; i++) {
      if (!devices[list[i]]) {
        this.requestDeviceStatus(list[i]);
      }
    }
    if (this.hasWhitelistedScenes(list)) {
      this.requestScenesStatus();
    }
  }

  removeDevice = (id) => {
    if (window.confirm('Remove this widget ?')) {
      const whitelist = this.state.whitelist.slice(0);
      whitelist.splice(whitelist.indexOf(id), 1);
      this.handleDeviceListChange(whitelist);
    }
  }

  requestDeviceStatus = (id) => {
    const explodedId = id.split('|');
    if (explodedId[0] !== 'd') {
      // Not a device type.
      return;
    }
    const idx = parseInt(id.split('|')[1], 10);
    this.mqtt.publish({'command': 'getdeviceinfo', 'idx': idx });
  }

  hasWhitelistedScenes(opt_list) {
    const list = opt_list || this.state.whitelist;
    return list.some((id) => (id[0] === 's' || id[0] === 'g'));
  }

  requestDeviceSpec = (id) => {
    const explodedId = id.split('|');
    if (explodedId[0] !== 'd') {
      // Not a device type.
      return;
    }
    this.json.get({type: 'devices', rid: explodedId[1] }, (data) =>{
      const deviceSpecs = Object.assign({}, this.state.deviceSpecs);
      deviceSpecs[id] = data.result[0];
      this.setState({deviceSpecs: deviceSpecs});
    });
  }

  requestScenesStatus = (opt_throttle = false) => {
    const getScenes = opt_throttle ?
        this.json.getAllScenesThrottled : this.json.getAllScenes;
    getScenes((data) => {
      if (data.status !== "OK") {
        alert("Unable to get scenes status.");
        return;
      }
      const scenes = data.result;
      const devices = Object.assign({}, this.state.devices);
      for (let i = 0; i < scenes.length; i++) {
        const scene = scenes[i];
        const fullId = (scene.Type === 'Group' ? 'g' : 's') + '|' + scene.idx;
        if (this.state.whitelist.indexOf(fullId) >= 0) {
          devices[fullId] = scene;
        }
      }
      this.setState({devices: devices});
    });
  }

  toggleMenu = () => {
    this.setState({ 'menuOpen': !this.state.menuOpen });
  }

  toggleSettings = () => {
    this.setState({
      'currentView': View.SERVER_SETTINGS,
      'layoutLocked': true });
  }

  toggleDeviceList = () => {
    this.setState({
      'currentView': View.DEVICE_LIST,
      'layoutLocked': true });
  }

  toggleAbout = () => {
    this.setState({
      'currentView': View.ABOUT,
      'layoutLocked': true });
  }

  toggleLayoutEdit = () => {
    this.setState({ 'layoutLocked': !this.state.layoutLocked });
  }

  toMainView = () => {
    this.setState({ 'currentView': View.DASHBOARD });
  }

  cleanupLayout(list) {
    // Clear layout items for devices that are no longer present.
    const ids = [];
    let maxY = -1;
    const updatedLayout = this.state.layout.filter(function(deviceLayout) {
      ids.push(deviceLayout.i);
      maxY = Math.max(deviceLayout.y, maxY);
      return list.indexOf(deviceLayout.i) >= 0;
    }, this);
    // Add missing layouts
    for (let i = 0; i < list.length; i++) {
      const deviceId = list[i];
      if (ids.indexOf(deviceId) < 0) {
        updatedLayout.push({x: 0, y: ++maxY, w: 2, h: 1, i: deviceId})
      }
    };
    this.setState({layout: updatedLayout});
    this.storeConfigChange({layout: updatedLayout});
  }

  onLayoutChange = (layout) => {
    this.setState({layout: layout});
    this.storeConfigChange({'layout': layout});
  }

  renderCurrentView = (opt_forceView = null) => {
    const view = opt_forceView || this.state.currentView;
    switch (view) {
      case View.ABOUT:
        return (<AboutView appState={this.state} themes={Themes}
            zoom={this.state.zoom}
            screensaverConfig={this.state.screensaverConfig}
            configName={this.state.configName}
            onThemeChange={this.handleThemeChange}
            onZoomChange={this.handleZoomChange}
            onScreensaverConfigChange={this.handleScreensaverConfigChange}
            multiConfig={this.state.multiConfig} />);
      case View.SERVER_SETTINGS:
        return (<SettingsView config={this.state.serverConfig}
            serverStatus={this.state.serverStatus}
            onChange={this.handleServerConfigChange}
            importConfigPrompt={this.importConfigPrompt} />);
      case View.DEVICE_LIST:
        return (<DeviceListView onWhitelistChange={this.handleDeviceListChange}
            idxWhitelist={this.state.whitelist} name={this.state.configName}
            onNameChange={this.handleConfigNameChange} />);
      default:
        if (this.state.whitelist.length === 0) {
          return (
            <div className="addDevices">
              {!this.state.multiConfig && <h2>Welcome to your Reacticz dashboard!</h2>}
              {this.state.multiConfig && <h2>This is a new blank dashboard!</h2>}
              {this.state.multiConfig && <p>Use the arrows (<svg className='icon'><use xlinkHref="#navigate-before" /></svg> and <svg className='icon'><use xlinkHref="#navigate-next" /></svg>) at the bottom to switch between your dashboards.</p>}
              <p>This dashboard is currently empty. Open the menu at the top right (<svg className='icon'><use xlinkHref="#settings" /></svg>) and go to the devices list screen (<svg className='icon'><use xlinkHref="#playlist-add-check" /></svg>) to select the widgets you want to add.</p>
              <p>Then come back here (<svg className='icon'><use xlinkHref="#home" /></svg>) and unlock the layout (<svg className='icon'><use xlinkHref="#lock" /></svg>) to drag and resize the widgets however you like.</p>
              <p>That's it!</p>
            </div>
          );
        }
        const widgets = this.state.layout.map(function(deviceLayout) {
          const deviceId = deviceLayout.i;
          const device = this.state.devices[deviceId];
          if (!device) {
            // Device not available
            return (<div key={deviceId} className="gridItem">
                <LoadingWidget onRemove={() => this.removeDevice(deviceId)}
                    theme={this.state.theme}/></div>);
          }
          let widget = '';
          if (device.Type === 'Group' || device.Type === 'Scene') {
            widget = <SceneWidget
                readOnly={!this.state.layoutLocked}
                key={'item'+ deviceId}
                scene={device}
                onChange={this.requestScenesStatus}
                theme={this.state.theme} />
          } else {
          widget = <DeviceWidget
              className="widget"
              layoutWidth={deviceLayout.w}
              readOnly={!this.state.layoutLocked}
              key={'item'+ deviceId}
              device={device}
              requestDeviceSpec={() => this.requestDeviceSpec(deviceId)}
              deviceSpec={this.state.deviceSpecs[deviceId]}
              theme={this.state.theme} />
          }
          return (
            <div key={deviceId} className={this.state.layoutLocked ? 'gridItem':'gridItem resizeable'}>
              {widget}
            </div>
          );
        }, this);
        const style = {
          fontSize: (100 * this.state.zoom) + '%'
        };
        return (
            <ResponsiveGridLayout
                layouts={{lg:this.state.layout}}
                onDragStop={this.onLayoutChange}
                onResizeStop={this.onLayoutChange}
                isDraggable={!this.state.layoutLocked}
                isResizable={!this.state.layoutLocked}
                compactType={null}
                breakpoints={{lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0}}
                cols={{lg: 12, md: 10, sm: 8, xs: 6, xxs: 4}}
                className="layout"
                style={style}
                items={this.state.whitelist.length}
                rowHeight={100}>
              {widgets}
            </ResponsiveGridLayout>);
    }
  }

  renderFooter(showFooter) {
      const footerStyle = {
        background: this.state.theme.overlayBackground,
        color: this.state.theme.overlayText,
        fill: this.state.theme.menuIcon,
        fontSize: (100 * this.state.zoom) + '%',
        height:(50 * this.state.zoom) + 'px',
        lineHeight: (50 * this.state.zoom) + 'px',
      };
      return (
          <div className={'footer' + (showFooter ? '' : ' hidden')} style={footerStyle}>
            <div className="left">
              {this.state.layoutLocked && this.state.multiConfig &&
                <button title="Previous Layout" onClick={this.getPrevConfig}>
                  <svg className="icon"><use xlinkHref="#navigate-before" /></svg>
                </button>
              }
              {!this.state.layoutLocked && this.state.multiConfig &&
                <button title="Delete" onClick={this.deleteCurrentConfig}>
                  <svg className="icon"><use xlinkHref="#delete" /></svg>
                </button>
              }
            </div>
            <div className="right">
              {this.state.layoutLocked && this.state.multiConfig &&
                <button title="Next Layout" onClick={this.getNextConfig}>
                  <svg className="icon"><use xlinkHref="#navigate-next" /></svg>
                </button>
              }
              {!this.state.layoutLocked &&
                <button title="Add a new dashboard" onClick={() => this.loadConfig(this.configHelper.addConfig())}>
                  <svg className="icon"><use xlinkHref="#add" /></svg>
                </button>
              }
            </div>
            <div className="title">{this.state.configName}</div>
          </div>);
  }

  render() {
    const shouldConfigure = !this.state.serverConfig.mqttBrokerUrl || !this.state.serverConfig.domoticzUrl;
    const view = this.renderCurrentView(shouldConfigure ? View.SERVER_SETTINGS : undefined);
    const currentView = this.state.currentView;
    const menuIconStyle = {
      fill: this.state.theme.menuIcon
    };
    const menuIconSelectedStyle = {
      fill: this.state.theme.menuIconSelected
    };
    const appStyle = {
      background: this.state.theme.appBackground,
      color: this.state.theme.appText,
      fill: this.state.theme.appText
    };

    const appBarStyle = {
      background: this.state.menuOpen ? this.state.theme.overlayBackground : '',
      display: shouldConfigure ? 'none' : ''
    };
    if (document) {
      document.body.style.background = this.state.theme.appBackground;
    }
    const showFooter = (currentView === View.DASHBOARD || currentView === View.DEVICE_LIST) && (this.state.multiConfig || !this.state.layoutLocked);

    return (
      <div className="App" style={appStyle}>
        <div key='menu' className={this.state.menuOpen ? 'appbar open' : 'appbar'} style={appBarStyle}>
          <button key='toggle' title='Menu' onClick={this.toggleMenu}>
            <svg className="icon" style={menuIconStyle}><use xlinkHref="#settings" /></svg>
          </button>
          {currentView !== View.DASHBOARD &&
               <button onClick={this.toMainView} title='Home'>
               <svg className='icon' style={menuIconStyle}><use xlinkHref="#home" /></svg></button>}
          {currentView === View.DASHBOARD &&
               <button onClick={this.toggleLayoutEdit} title='Layout lock'>
               <svg className='icon' style={(currentView === View.DASHBOARD ? menuIconSelectedStyle : menuIconStyle)}><use xlinkHref={this.state.layoutLocked ? '#lock' : '#lock-open'} /></svg></button>}
          <button onClick={this.toggleDeviceList} title='Device selection'>
            <svg className='icon' style={(currentView === View.DEVICE_LIST ? menuIconSelectedStyle : menuIconStyle)}><use xlinkHref='#playlist-add-check' /></svg>
          </button>
          <button onClick={this.toggleSettings} title='Server settings'>
            <svg className='icon' style={(currentView === View.SERVER_SETTINGS ? menuIconSelectedStyle : menuIconStyle)}><use xlinkHref='#router' /></svg>
          </button>
          <button onClick={this.toggleAbout} title='About'>
            <svg className='icon' style={(currentView === View.ABOUT ? menuIconSelectedStyle : menuIconStyle)}><use xlinkHref='#info-outline' /></svg>
          </button>
        </div>
        {view}
        {this.renderFooter(showFooter)}
        {this.state.screensaverConfig.delay >= 0 && <Screensaver type={this.state.screensaverConfig.type} delay={this.state.screensaverConfig.delay} theme={this.state.theme} />}
      </div>
    );
  }
}

export default App;


<|start_filename|>src/widgets/TemperatureWidget.css<|end_filename|>
.TemperatureWidget .temp::after {
  content: '°';
}

.TemperatureWidget .hum::after {
  content: '% hum.';
}

.TemperatureWidget .humonly::after {
  content: '%';
}


<|start_filename|>src/widgets/MediaPlayer.js<|end_filename|>
import React, { Component } from 'react';
import MqttClientSingleton from '../util/MqttClientSingleton'
import './SwitchOnOff.css';
import './MediaPlayer.css';

class MediaPlayer extends Component {

  constructor(props) {
    super(props);
    this.mqtt = new MqttClientSingleton();
  }

  handleClick = (event) => {
    if (this.props.readOnly) {
      return
    }
    const message = {
      command: 'switchlight',
      idx: this.props.idx,
      switchcmd: this.props.value === 0 ? 'On' : 'Off',
      level: 100
    };
    this.mqtt.publish(message);
  }

  getButtonStyle() {
    const theme = this.props.theme;
    if (!theme) {
      return {};
    }
    const style = {
      color: theme.textOn,
      background: this.props.readOnly ? theme.unlockedBackground : theme.buttonOn
    }
    if (this.props.valueText === "Off" || this.props.value === 0) {
      style.color = theme.textOff;
      style.background =  this.props.readOnly ? theme.unlockedBackground : theme.buttonOff;
    }
    return style;
  }

  render() {
    const valueText = this.props.valueText || (this.props.value === 0 ? 'Off' : 'On');
    const valuePlaying = ((this.props.playing === "100" && valueText === "On") ? 'Loading...' : ((this.props.playing === "100" && valueText === "Off") ? 'Off' : this.props.playing));
    return (
      <button className="switch" style={this.getButtonStyle()} onClick={this.handleClick} title={valueText}>{this.props.label}<i className="playing">{valuePlaying}</i></button>
    );
  }

}

export default MediaPlayer


<|start_filename|>src/widgets/LoadingWidget.css<|end_filename|>
.LoadingWidget {
  color: #808080;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  height: 100%;
  width: 100%;
}

.LoadingWidget  button {
  padding: 10px;
  margin-top: 10px;
  color: red;
  background-color: #ffe0e0;
  border: none;
  outline: none;
  transition: all .2s ease-out;
  font-size: 1em;
  font-weight: lighter;
  text-overflow: ellipsis;
  overflow: hidden;
}

.LoadingWidget button:active {
  transform: scale(0.9);
}



<|start_filename|>src/widgets/SwitchBlinds.css<|end_filename|>
.SwitchBlinds {
  height: 100%;
  display: flex;
  flex-direction: column;
}

.SwitchBlinds section {
  display: flex;
  flex-grow: 1;
}

.SwitchBlinds h2 {
  width: 100%;
  padding: 10px;
  margin: 0;
  font-size: 1em;
  font-weight: lighter;
  text-align: center;
  white-space: nowrap;
  box-sizing: border-box;
  pointer-events: none;
  text-overflow: ellipsis;
  overflow: hidden;
  z-index: 1;
}

.blindsOpen, .blindsStop, .blindsClose {
  flex-grow: 1;
  width: 33%;
  border: none;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: transform .3s ease-out;
}

.SwitchBlinds button:active {
  transform: scale(0.9);
  outline: none;
}

.SwitchBlinds button:focus {
  outline: none;
}

.blindsStop {
  margin: 0 10px;
}



<|start_filename|>src/setupTests.js<|end_filename|>
import React from 'react';
import ReactDOM from 'react-dom';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import JSONClientSingleton from './util/JSONClientSingleton';
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({ adapter: new Adapter() });

const localStorageMock = {
  __dataStore: [],
  getItem: jest.fn(function(key) {
    return this.__dataStore[key] || null
  }),
  removeItem: jest.fn(function(key) { delete this.__dataStore[key] }),
  setItem: jest.fn(function(key, val) {
    this.__dataStore[key] = val
  })
};
global.localStorage = localStorageMock;

const testJSONClientSingleton = new JSONClientSingleton();
testJSONClientSingleton.setServerUrl('http://ac.me:42');

// This sets the mock adapter on the default instance
const axiosMock = new MockAdapter(axios);

// Mock any GET request to /users
// arguments for reply are (status, data, headers)
axiosMock.onGet().reply(function(config) {
  switch (config.url) {
    case 'http://ac.me:42/json.htm?type=devices&filter=all&used=true&order=Name':
    case 'http://ac.me:42/json.htm?type=scenes':
    case 'http://ac.me:42/json.htm?type=plans&order=name&used=true':
      return [200, {result:[]}];
    default:
      return [404];
  }
});

// The welcome message when there is no config. Used in multiple tests.
global.welcomeMessage = <p>Please setup your server config to proceed.</p>;


<|start_filename|>src/widgets/MeterWidget.js<|end_filename|>
import React, { Component } from 'react';
import GenericWidget from './helpers/GenericWidget';

class MeterWidget extends Component {

  render() {
    return (
      <GenericWidget class="MeterWidget" icon="equalizer"
          isOn={Number(this.props.value) > 0 }
          value1={this.props.value +
              (this.props.unit ? ' ' + this.props.unit : '')}
          {...this.props} />
    );
  }

}

export default MeterWidget


<|start_filename|>src/SettingsView.test.js<|end_filename|>
import React from 'react';
import ReactDOM from 'react-dom';
import { mount } from 'enzyme';
import SettingsView from './SettingsView';

it('renders without crashing & shows welcome message', () => {
  const wrapper = mount(<SettingsView />);
  expect(wrapper.contains(welcomeMessage)).toEqual(true);
});

it('only shows the welcome message if there is no existing config', () => {
  const wrapper = mount(<SettingsView config={{mqttBrokerUrl: 'ws://dummy', domoticzUrl: 'http://foobar'}}/>);
  expect(wrapper.contains(welcomeMessage)).toEqual(false);
});


<|start_filename|>src/widgets/SwitchRGBW.js<|end_filename|>
import React, { Component } from 'react';
import Slider from './helpers/Slider';
import JSONClientSingleton from '../util/JSONClientSingleton'

import './SwitchRGBW.css';
import './SwitchOnOff.css';

class SwitchRGBW extends Component {

  constructor(props) {
    super(props);
    this.state = {
      color: '',
      hue: 0,
      sat: 0
    }
    this.json = new JSONClientSingleton();
  }

  toggle = () => {
    if (this.props.readOnly) {
      return
    }
    let message = {
      type: 'command',
      param: 'switchlight',
      switchcmd: 'Toggle',
      idx: this.props.idx
    }
    this.json.get(message);
  }

  toHexStr(num) {
    const hex = Math.round(num * 255).toString(16);
    return hex.length < 2 ? '0' + hex : hex;
  }

  hslToHex(hue = 0, sat = 1, lum = 0.5) {
    let r, g, b;
    if (sat === 0) {
        r = g = b = lum; // achromatic
    } else {
      const hue2rgb = function hue2rgb(p, q, t){
        if (t < 0) t += 1;
        if (t > 1) t -= 1;
        if (t < 1/6) return p + (q - p) * 6 * t;
        if (t < 1/2) return q;
        if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
        return p;
      }
      const q = lum < 0.5 ? lum * (1 + sat) : lum + sat - lum * sat;
      const p = 2 * lum - q;
      r = hue2rgb(p, q, hue + 1/3);
      g = hue2rgb(p, q, hue);
      b = hue2rgb(p, q, hue - 1/3);
    }
    return '#' + this.toHexStr(r) + this.toHexStr(g) + this.toHexStr(b);
  }

  changeHue = (value) => {
    this.setState({hue: value, sat: 1});
  }

  changeColor = (color) => {
    if (this.props.readOnly) {
      return
    }
    this.setState({color: color});
    let message = {
      type: 'command',
      param: 'setcolbrightnessvalue',
      idx: this.props.idx,
      hex: color.replace('#',''),
      brightness: 100,
      iswhite: false
    };
    this.json.get(message);
  }

  toWhite = () => {
    if (this.props.readOnly) {
      return;
    }
    this.setState({sat: 0});
    this.changeColor('#ffffff');
  }

  render() {
    const theme = this.props.theme;
    const style = {
      background: this.props.readOnly ? theme.unlockedBackground : theme.background
    };
    const buttonStyle = theme ? {
      background: this.props.value === 0 ? theme.buttonOff : theme.buttonOn,
      color: this.props.value === 0 ? theme.textOff : theme.textOn
    } : {};
    const value = this.props.value === 0 ? 'Off' : 'On';
    const lv1 = this.hslToHex(this.state.hue, this.state.sat,
        this.state.sat ? .125 : .2);
    const lv2 = this.hslToHex(this.state.hue, this.state.sat,
        this.state.sat ? .25 : .4);
    const lv3 = this.hslToHex(this.state.hue, this.state.sat,
        this.state.sat ? .375 : .6);
    const lv4 = this.hslToHex(this.state.hue, this.state.sat,
        this.state.sat ? .5 : .8);
    return (
      <div className="SwitchRGBW" style={style}>
        <button className="toggle" style={buttonStyle} title={value} onClick={this.toggle}>{this.props.label}</button>
        {this.props.layoutWidth > 1 && <div className="colorSelect">
          <div className="swatch">
            <button title="Color at 25%" style={{background: lv1}} onClick={() => this.changeColor(lv1)}></button>
            <button title="Color at 50%" style={{background: lv2}} onClick={() => this.changeColor(lv2)}></button>
            <button title="Color at 75%" style={{background: lv3}} onClick={() => this.changeColor(lv3)}></button>
            <button title="Color at 100%" style={{background: lv4}} onClick={() => this.changeColor(lv4)}></button>
            <div style={{background: theme.buttonOn}}><button className="white" style={{borderColor: theme.buttonOn}} title="Return to white" onClick={this.toWhite}></button></div>
          </div>
          <div className="hue">
            <Slider disabled={this.props.readOnly} edgeTolerance={0.1}
                onChange={this.changeHue} value={this.state.hue} />
          </div>
        </div>}
      </div>
    );
  }

}

export default SwitchRGBW


<|start_filename|>src/widgets/MotionSensorWidget.js<|end_filename|>
import React, { Component } from 'react';
import GenericWidget from './helpers/GenericWidget';

class MotionSensorWidget extends Component {

  render() {
    return (
      <GenericWidget icon="run-fast"
        isOn={this.props.value > 0}
        value1={this.props.value === 0 ? 'No motion' : 'Motion'}
        {...this.props} />
      );
  }
}

export default MotionSensorWidget


<|start_filename|>src/widgets/helpers/GenericWidget.css<|end_filename|>
.GenericWidget {
  display: flex;
  height: 100%;
  align-items: center;
}

.GenericWidget h2,
.GenericWidget div {
  font-weight: lighter;
  overflow: hidden;
  text-overflow: ellipsis;
}

.GenericWidget h2 {
  margin: 5px 0;
  font-size: 1.25em;
}

.GenericWidget section {
  padding: 10px;
  flex: 2;
}

.GenericWidget svg {
  height: 100%;
  flex: 1;
}


<|start_filename|>src/widgets/WattWidget.js<|end_filename|>
import React, { Component } from 'react';
import GenericWidget from './helpers/GenericWidget';
import './WattWidget.css';

class WattWidget extends Component {

  render() {
    let prod = this.props.prodValue !== undefined;
    return (
      <GenericWidget class={'WattWidget' + (prod ? ' withProd' : '')}
          icon="power"
          isOn={Number(this.props.powerValue) > 0 }
          value1={Number(this.props.powerValue).toFixed(1)}
          value2={prod ?
            Number(this.props.prodValue).toFixed(1) :
            Number(this.props.energyValue/1000).toFixed(3)}
          {...this.props} />
    );
  }

}

export default WattWidget


<|start_filename|>src/widgets/SwitchSelector.js<|end_filename|>
import React, { Component } from 'react';
import LoadingWidget from './LoadingWidget'
import MqttClientSingleton from '../util/MqttClientSingleton'
import './SwitchSelector.css';
import './SwitchOnOff.css';

class SwitchSelector extends Component {

  constructor(props) {
    super(props);
    this.mqtt = new MqttClientSingleton();
  }

  handleSelect = (event) => {
    if (this.props.readOnly) {
      return
    }
    const message = {
      command: 'switchlight',
      idx: this.props.idx,
      switchcmd: 'Set Level',
      level: event.target.value
    };
    this.mqtt.publish(message);
  }

  render() {
    if (!this.props.deviceSpec) {
      return <LoadingWidget />
    }
    const theme = this.props.theme;
    const style = theme ? {
      background: this.props.readOnly ? theme.unlockedBackground : theme.background,
      color: theme.text
    } : {};
    const buttonStyleOff = theme ? {
      background: theme.buttonOff,
      color: theme.textOff
    } : {};
    const buttonStyleOn = theme ? {
      background: theme.buttonOn,
      color: theme.textOn
    } : {};
    const carretStyle = theme ? {
      color: this.props.value === '0' ? theme.textOff : theme.textOn
    } : {};
    const list = this.props.levels.map(function(level, index) {
      const levelValue = index * 10;
      const selected = parseInt(this.props.value, 10) === levelValue;
      if (index === 0 && this.props.deviceSpec["LevelOffHidden"] === true) {
        return null;
      }
      if (this.props.useButtons) {
        return (
          <button key={index + '-' + level}
                  className="switch"
                  style={selected ? buttonStyleOn : buttonStyleOff}
                  value={levelValue} onClick={this.handleSelect}>{level}
          </button>
        );
      }
      return (
        <option key={index + '-' + level} value={levelValue}>{level}</option>
      );
    }, this);
    if (this.props.useButtons) {
      return (<div className="selector" style={style}><h2>{this.props.label}</h2><section>{list}</section></div>);
    }
    return (<div className="selector" style={style}>
        <h2>{this.props.label}</h2>
        <div className="selectContainer">
        <i className="carret" style={carretStyle}>▼</i>
        <select disabled={this.props.readOnly} value={this.props.value} style={this.props.value === '0' ? buttonStyleOff : buttonStyleOn} onChange={this.handleSelect}>{list}</select>
        </div></div>);
  }
}

export default SwitchSelector


<|start_filename|>src/App.test.js<|end_filename|>
import React from 'react';
import ReactDOM from 'react-dom';
import { mount } from 'enzyme';
import App from './App';

it('renders without crashing', () => {
  const wrapper = mount(<App />);
  expect(wrapper.contains(welcomeMessage)).toEqual(true);
  wrapper.unmount();
});

it('reads config from localstorage', () => {
  var serverConfig = {
    mqttBrokerUrl: 'ws://dummy-broker:9001',
    domoticzUrl: 'http://dummy-domoticz:8080'
  };
  global.localStorage.setItem('serverConfig', JSON.stringify(serverConfig));
  const div = document.createElement('div');
  const wrapper = mount(<App />);
  const settings = <h2>Welcome to your Reacticz dashboard!</h2>;
  expect(wrapper.contains(settings)).toEqual(true);
  expect(wrapper.state('serverConfig')).toEqual(serverConfig);
  wrapper.unmount();
});



<|start_filename|>src/Screensaver.js<|end_filename|>
import React, { Component } from 'react';
import Clock from 'react-clock';

import './Screensaver.css';

const MONITORED_EVENTS = ["touchstart", "touchmove", "mousedown", "mousemove", "click"];
const DEBOUNCE_DELAY_MS = 350;

export const TYPE = {
  CLOCK: "clock",
  BLANK: "blank"
};

class Screensaver extends Component {
  constructor(props) {
    super(props);
    this.state = {
      visible: false,
      activityTimeoutId: null,
      date: new Date(),
      dateIntervalId: null,
      debounceTimeoutId: null,
      size: Math.min(window.innerHeight, window.innerWidth) * .6
    }
    this.unmounted = false;
  }

  stopTimer = () => {
    window.clearTimeout(this.state.activityTimeoutId);
  }

  restartTimer = () => {
    this.stopTimer();
    if (this.props.delay) {
      this.setState({activityTimeoutId: window.setTimeout(this.show, this.props.delay * 1000)});
    }
  }

  componentWillMount() {
    for (let i in MONITORED_EVENTS) {
      window.addEventListener(MONITORED_EVENTS[i], this.onMove, { passive: false });
    }
    this.setState({dateIntervalId: window.setInterval(() => this.setState({ date: new Date() }), 1000)});
    this.restartTimer();
  }

  componentWillUnmount() {
    this.unmounted = true;
    this.stopTimer();
    window.clearTimeout(this.state.debounceTimeoutId);
    window.clearInterval(this.state.dateIntervalId);
    for (let i in MONITORED_EVENTS) {
      window.removeEventListener(MONITORED_EVENTS[i], this.onMove, { passive: false });
    }
  }

  show = () => {
    this.setState({visible: true});
  }

  onMove = (event) => {
    if (this.state.visible) {
      event.stopPropagation();
      event.preventDefault();
    }
    this.stopTimer();
    this.setState({debounceTimeoutId: window.setTimeout(() => {
      if (this.unmounted) {
        return;
      }
      this.restartTimer();
      this.setState({visible: false});
    }, DEBOUNCE_DELAY_MS)});
  }

  render() {
    if (!this.state.visible) {
      return null;
    }
    return (
      <div className="Screensaver" style={{backgroundColor: this.props.type === TYPE.CLOCK ? this.props.theme.appBackground : ''}}>
        {this.props.type === TYPE.CLOCK && <Clock className="Clock"
            value={this.state.date} 
            size={this.state.size} 
            hourHandLength={60}
            hourHandOppositeLength={20}
            hourHandWidth={8}
            hourMarksLength={20}
            hourMarksWidth={8}
            minuteHandLength={90}
            minuteHandOppositeLength={20}
            minuteHandWidth={6}
            minuteMarksLength={6}
            minuteMarksWidth={3}
            secondHandLength={75}
            secondHandOppositeLength={25}
            secondHandWidth={3}
          />}
      </div>
    );
  }

}

export default Screensaver


<|start_filename|>src/widgets/MediaPlayer.css<|end_filename|>
.playing {
  font-size: 1em;
  display: block;
}


<|start_filename|>src/widgets/CustomSensorWidget.js<|end_filename|>
import React, { Component } from 'react';
import GenericWidget from './helpers/GenericWidget';
import LoadingWidget from './LoadingWidget'

class CustomSensorWidget extends Component {

  render() {
    if (!this.props.deviceSpec) {
      return <LoadingWidget />
    }
    return (
      <GenericWidget icon="information-variant"
          isOn={Number(this.props.value) > 0}
          value1={[this.props.value, " ", this.props.deviceSpec['SensorUnit']]}
          {...this.props} />
    );
  }

}

export default CustomSensorWidget


<|start_filename|>src/widgets/SwitchOnOff.css<|end_filename|>
button.switch {
  display: block;
  width: 100%;
  height: 100%;
  border: none;
  outline: none;
  transition: all .2s ease-out;
  font-size: 1.25em;
  font-weight: lighter;
  text-overflow: ellipsis;
  overflow: hidden;
}

button.switch:active {
  transform: scale(0.9);
}



<|start_filename|>src/AboutView.css<|end_filename|>
.AboutView {
  margin: 50px 20px;
}

.AboutView button, .AboutView select {
  color: inherit;
  background: rgba(211, 211, 211, .5);
  border: 1px solid #d3d3d3;
}

.exportOptions {
  display: flex;
  flex-direction: column;
}

.AboutView em {
  margin: 5px 0;
}

.exportUrl {
  display: flex;
  margin: 15px 0;
  max-width: 640px;
}

.url {
  margin: 0;
  width: 100%;
  font-size: 1em;
  font-family: monospace;
  text-overflow: ellipsis;
}

.exportUrl button {
  width: 100px;
  height: 40px;
}

.zoomrange {
  vertical-align: middle;
}

.aboutHeader {
  display: flex;
  align-items: center;
}

.aboutHeader img {
  image-rendering: pixelated;
  height: 100%;
  margin-right: 10px;
}

.aboutHeader h1 {
  display: inline-block;
  margin: 0 0 5px 0;
  font-size: 20px;
}

#delayInput {
  width: 45px;
  margin-left: 10px;
}

<|start_filename|>src/widgets/TextWidget.js<|end_filename|>
import React, { Component } from 'react';
import GenericWidget from "./helpers//GenericWidget";

class TextWidget extends Component {

  render() {
    const textBits = [];
    const device = this.props.device;
    for (const key in device) {
      if ({}.hasOwnProperty.call(device, key) && !!key.match(/^svalue/)) {
        textBits.push(device[key]);
      }
    }
    // For some reason Domoticz gives me incomplete "&nbsp" (without ";") HTML
    // codes in one of my virtual devices.  So we are removing them here.
    const text = textBits.join(' ').replace(/&nbsp/g, ' ');
    return (
      <GenericWidget value1={text} {...this.props} />
    );
  }

}

export default TextWidget
