From f43d9ba6805b0b11f078dd255b13699a3d793a00 Mon Sep 17 00:00:00 2001
From: William Scanlon <william.joseph.scanlon@gmail.com>
Date: Thu, 13 Sep 2018 03:35:40 -0400
Subject: [PATCH] Support for the Quirky Nimbus (#16520)

* Support for quriky nimbu

* Fixed lint

* fixed some typos
---
 homeassistant/components/climate/wink.py    |  10 +-
 homeassistant/components/wink/__init__.py   | 163 ++++++++++++++++++--
 homeassistant/components/wink/services.yaml |  41 +++++
 requirements_all.txt                        |   2 +-
 4 files changed, 199 insertions(+), 17 deletions(-)

diff --git a/homeassistant/components/climate/wink.py b/homeassistant/components/climate/wink.py
index d8e6843bec8..3013a155380 100644
--- a/homeassistant/components/climate/wink.py
+++ b/homeassistant/components/climate/wink.py
@@ -118,7 +118,7 @@ class WinkThermostat(WinkDevice, ClimateDevice):
                 self.hass, self.target_temperature_low, self.temperature_unit,
                 PRECISION_TENTHS)
 
-        if self.external_temperature:
+        if self.external_temperature is not None:
             data[ATTR_EXTERNAL_TEMPERATURE] = show_temp(
                 self.hass, self.external_temperature, self.temperature_unit,
                 PRECISION_TENTHS)
@@ -126,16 +126,16 @@ class WinkThermostat(WinkDevice, ClimateDevice):
         if self.smart_temperature:
             data[ATTR_SMART_TEMPERATURE] = self.smart_temperature
 
-        if self.occupied:
+        if self.occupied is not None:
             data[ATTR_OCCUPIED] = self.occupied
 
-        if self.eco_target:
+        if self.eco_target is not None:
             data[ATTR_ECO_TARGET] = self.eco_target
 
-        if self.heat_on:
+        if self.heat_on is not None:
             data[ATTR_HEAT_ON] = self.heat_on
 
-        if self.cool_on:
+        if self.cool_on is not None:
             data[ATTR_COOL_ON] = self.cool_on
 
         current_humidity = self.current_humidity
diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py
index c996572bf51..0399b25b278 100644
--- a/homeassistant/components/wink/__init__.py
+++ b/homeassistant/components/wink/__init__.py
@@ -26,7 +26,7 @@ from homeassistant.helpers.entity_component import EntityComponent
 from homeassistant.helpers.event import track_time_interval
 from homeassistant.util.json import load_json, save_json
 
-REQUIREMENTS = ['python-wink==1.9.1', 'pubnubsub-handler==1.0.2']
+REQUIREMENTS = ['python-wink==1.10.1', 'pubnubsub-handler==1.0.2']
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -73,11 +73,25 @@ SERVICE_SET_AUTO_SHUTOFF = "siren_set_auto_shutoff"
 SERVICE_SIREN_STROBE_ENABLED = "set_siren_strobe_enabled"
 SERVICE_CHIME_STROBE_ENABLED = "set_chime_strobe_enabled"
 SERVICE_ENABLE_SIREN = "enable_siren"
+SERVICE_SET_DIAL_CONFIG = "set_nimbus_dial_configuration"
+SERVICE_SET_DIAL_STATE = "set_nimbus_dial_state"
 
 ATTR_VOLUME = "volume"
 ATTR_TONE = "tone"
 ATTR_ENABLED = "enabled"
 ATTR_AUTO_SHUTOFF = "auto_shutoff"
+ATTR_MIN_VALUE = "min_value"
+ATTR_MAX_VALUE = "max_value"
+ATTR_ROTATION = "rotation"
+ATTR_SCALE = "scale"
+ATTR_TICKS = "ticks"
+ATTR_MIN_POSITION = "min_position"
+ATTR_MAX_POSITION = "max_position"
+ATTR_VALUE = "value"
+ATTR_LABELS = "labels"
+
+SCALES = ["linear", "log"]
+ROTATIONS = ["cw", "ccw"]
 
 VOLUMES = ["low", "medium", "high"]
 TONES = ["doorbell", "fur_elise", "doorbell_extended", "alert",
@@ -145,6 +159,23 @@ ENABLED_SIREN_SCHEMA = vol.Schema({
     vol.Required(ATTR_ENABLED): cv.boolean
 })
 
+DIAL_CONFIG_SCHEMA = vol.Schema({
+    vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
+    vol.Optional(ATTR_MIN_VALUE): vol.Coerce(int),
+    vol.Optional(ATTR_MAX_VALUE): vol.Coerce(int),
+    vol.Optional(ATTR_MIN_POSITION): cv.positive_int,
+    vol.Optional(ATTR_MAX_POSITION): cv.positive_int,
+    vol.Optional(ATTR_ROTATION): vol.In(ROTATIONS),
+    vol.Optional(ATTR_SCALE): vol.In(SCALES),
+    vol.Optional(ATTR_TICKS): cv.positive_int
+})
+
+DIAL_STATE_SCHEMA = vol.Schema({
+    vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
+    vol.Required(ATTR_VALUE): vol.Coerce(int),
+    vol.Optional(ATTR_LABELS): cv.ensure_list(cv.string)
+})
+
 WINK_COMPONENTS = [
     'binary_sensor', 'sensor', 'light', 'switch', 'lock', 'cover', 'climate',
     'fan', 'alarm_control_panel', 'scene'
@@ -432,8 +463,23 @@ def setup(hass, config):
             DOMAIN, SERVICE_SET_PAIRING_MODE, set_pairing_mode,
             schema=SET_PAIRING_MODE_SCHEMA)
 
-    def service_handle(service):
-        """Handle services."""
+    def nimbus_service_handle(service):
+        """Handle nimbus services."""
+        entity_id = service.data.get('entity_id')[0]
+        _all_dials = []
+        for sensor in hass.data[DOMAIN]['entities']['sensor']:
+            if isinstance(sensor, WinkNimbusDialDevice):
+                _all_dials.append(sensor)
+        for _dial in _all_dials:
+            if _dial.entity_id == entity_id:
+                if service.service == SERVICE_SET_DIAL_CONFIG:
+                    _dial.set_configuration(**service.data)
+                if service.service == SERVICE_SET_DIAL_STATE:
+                    _dial.wink.set_state(service.data.get("value"),
+                                         service.data.get("labels"))
+
+    def siren_service_handle(service):
+        """Handle siren services."""
         entity_ids = service.data.get('entity_id')
         all_sirens = []
         for switch in hass.data[DOMAIN]['entities']['switch']:
@@ -495,41 +541,68 @@ def setup(hass, config):
     if sirens:
 
         hass.services.register(DOMAIN, SERVICE_SET_AUTO_SHUTOFF,
-                               service_handle,
+                               siren_service_handle,
                                schema=SET_AUTO_SHUTOFF_SCHEMA)
 
         hass.services.register(DOMAIN, SERVICE_ENABLE_SIREN,
-                               service_handle,
+                               siren_service_handle,
                                schema=ENABLED_SIREN_SCHEMA)
 
     if has_dome_or_wink_siren:
 
         hass.services.register(DOMAIN, SERVICE_SET_SIREN_TONE,
-                               service_handle,
+                               siren_service_handle,
                                schema=SET_SIREN_TONE_SCHEMA)
 
         hass.services.register(DOMAIN, SERVICE_ENABLE_CHIME,
-                               service_handle,
+                               siren_service_handle,
                                schema=SET_CHIME_MODE_SCHEMA)
 
         hass.services.register(DOMAIN, SERVICE_SET_SIREN_VOLUME,
-                               service_handle,
+                               siren_service_handle,
                                schema=SET_VOLUME_SCHEMA)
 
         hass.services.register(DOMAIN, SERVICE_SET_CHIME_VOLUME,
-                               service_handle,
+                               siren_service_handle,
                                schema=SET_VOLUME_SCHEMA)
 
         hass.services.register(DOMAIN, SERVICE_SIREN_STROBE_ENABLED,
-                               service_handle,
+                               siren_service_handle,
                                schema=SET_STROBE_ENABLED_SCHEMA)
 
         hass.services.register(DOMAIN, SERVICE_CHIME_STROBE_ENABLED,
-                               service_handle,
+                               siren_service_handle,
                                schema=SET_STROBE_ENABLED_SCHEMA)
 
     component.add_entities(sirens)
 
+    nimbi = []
+    dials = {}
+    all_nimbi = pywink.get_cloud_clocks()
+    all_dials = []
+    for nimbus in all_nimbi:
+        if nimbus.object_type() == "cloud_clock":
+            nimbi.append(nimbus)
+            dials[nimbus.object_id()] = []
+    for nimbus in all_nimbi:
+        if nimbus.object_type() == "dial":
+            dials[nimbus.parent_id()].append(nimbus)
+
+    for nimbus in nimbi:
+        for dial in dials[nimbus.object_id()]:
+            all_dials.append(WinkNimbusDialDevice(nimbus, dial, hass))
+
+    if nimbi:
+        hass.services.register(DOMAIN, SERVICE_SET_DIAL_CONFIG,
+                               nimbus_service_handle,
+                               schema=DIAL_CONFIG_SCHEMA)
+
+        hass.services.register(DOMAIN, SERVICE_SET_DIAL_STATE,
+                               nimbus_service_handle,
+                               schema=DIAL_STATE_SCHEMA)
+
+    component.add_entities(all_dials)
+
     return True
 
 
@@ -596,6 +669,7 @@ class WinkDevice(Entity):
                                                self.wink.name())
 
     def _pubnub_update(self, message):
+        _LOGGER.debug(message)
         try:
             if message is None:
                 _LOGGER.error("Error on pubnub update for %s "
@@ -740,3 +814,70 @@ class WinkSirenDevice(WinkDevice):
             attributes["chime_mode"] = chime_mode
 
         return attributes
+
+
+class WinkNimbusDialDevice(WinkDevice):
+    """Representation of the Quirky Nimbus device."""
+
+    def __init__(self, nimbus, dial, hass):
+        """Initialize the Nimbus dial."""
+        super().__init__(dial, hass)
+        self.parent = nimbus
+
+    @asyncio.coroutine
+    def async_added_to_hass(self):
+        """Call when entity is added to hass."""
+        self.hass.data[DOMAIN]['entities']['sensor'].append(self)
+
+    @property
+    def state(self):
+        """Return dials current value."""
+        return self.wink.state()
+
+    @property
+    def name(self):
+        """Return the name of the device."""
+        return self.parent.name() + " dial " + str(self.wink.index() + 1)
+
+    @property
+    def device_state_attributes(self):
+        """Return the device state attributes."""
+        attributes = super(WinkNimbusDialDevice, self).device_state_attributes
+        dial_attributes = self.dial_attributes()
+
+        return {**attributes, **dial_attributes}
+
+    def dial_attributes(self):
+        """Return the dial only attributes."""
+        return {
+            "labels": self.wink.labels(),
+            "position": self.wink.position(),
+            "rotation": self.wink.rotation(),
+            "max_value": self.wink.max_value(),
+            "min_value": self.wink.min_value(),
+            "num_ticks": self.wink.ticks(),
+            "scale_type": self.wink.scale(),
+            "max_position": self.wink.max_position(),
+            "min_position": self.wink.min_position()
+        }
+
+    def set_configuration(self, **kwargs):
+        """
+        Set the dial config.
+
+        Anything not sent will default to current setting.
+        """
+        attributes = {**self.dial_attributes(), **kwargs}
+
+        min_value = attributes["min_value"]
+        max_value = attributes["max_value"]
+        rotation = attributes["rotation"]
+        ticks = attributes["num_ticks"]
+        scale = attributes["scale_type"]
+        min_position = attributes["min_position"]
+        max_position = attributes["max_position"]
+
+        self.wink.set_configuration(min_value, max_value, rotation,
+                                    scale=scale, ticks=ticks,
+                                    min_position=min_position,
+                                    max_position=max_position)
diff --git a/homeassistant/components/wink/services.yaml b/homeassistant/components/wink/services.yaml
index 1dc4ecf959b..a3b489f9cf5 100644
--- a/homeassistant/components/wink/services.yaml
+++ b/homeassistant/components/wink/services.yaml
@@ -111,3 +111,44 @@ set_chime_volume:
     volume:
         description: Volume level. One of ["low", "medium", "high"]
         example: "low"
+
+set_nimbus_dial_configuration:
+  description: Set the configuration of an individual nimbus dial
+  fields:
+    entity_id:
+      description: Name of the entity to set.
+      example: 'wink.nimbus_dial_3'
+    rotation:
+      description: Direction dial hand should spin ["cw" or "ccw"]
+      example: 'cw'
+    ticks:
+      description: Number of times the hand should move
+      example: 12
+    scale:
+      description: How the dial should move in response to higher values ["log" or "linear"]
+      example: "linear"
+    min_value:
+      description: The minimum value allowed to be set
+      example: 0
+    max_value:
+      description: The maximum value allowd to be set
+      example: 500
+    min_position:
+      description: The minimum position the dial hand can rotate to generally [0-360]
+      example: 0
+    max_position:
+      description: The maximum position the dial hand can rotate to generally [0-360]
+      example: 360
+
+set_nimbus_dial_state:
+  description: Set the value and lables of an individual nimbus dial
+  fields:
+    entity_id:
+      description: Name fo the entity to set.
+      example: 'wink.nimbus_dial_3'
+    value:
+      description: The value that should be set (Should be between min_value and max_value)
+      example: 250
+    labels:
+      description: The values shown on the dial labels ["Dial 1", "test"] the first value is what is shown by default the second value is shown when the nimbus is pressed
+      example: ["example", "test"]
\ No newline at end of file
diff --git a/requirements_all.txt b/requirements_all.txt
index 933245e02e8..a5936d14169 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -1160,7 +1160,7 @@ python-velbus==2.0.19
 python-vlc==1.1.2
 
 # homeassistant.components.wink
-python-wink==1.9.1
+python-wink==1.10.1
 
 # homeassistant.components.sensor.swiss_public_transport
 python_opendata_transport==0.1.3
-- 
GitLab