[
  {
    "path": ".drone.yml",
    "content": "kind: pipeline\nname: default\n\nsteps:\n- name: publish\n  image: plugins/pypi\n  settings:\n    username:\n      from_secret: pypi_username\n    password:\n      from_secret: pypi_password\n    distributions:\n    - sdist\n    - bdist_wheel\n  when:\n    ref:\n    - refs/tags/*.*.*\n"
  },
  {
    "path": ".gitignore",
    "content": ".idea\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"cSpell.words\": [\n        \"annekov\",\n        \"pavlo\",\n        \"tuya\",\n        \"tuyacloudurl\",\n        \"tuyaha\",\n        \"tuyapy\"\n    ]\n}"
  },
  {
    "path": "LICENSE",
    "content": "Copyright for portions of project tuyaha are held by Tuya Inc., 2018 as part\nof project tuyapy. All other copyright for project tuyaha are held by\nPavlo Annekov, 2019.\n\nMIT License\n\nCopyright (c) 2019 Pavlo Annekov\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include LICENSE\n"
  },
  {
    "path": "README.md",
    "content": "# tuyaha\n\n## This library was maintained exclusively for Home Assistant Tuya component. As HA 2021.10.0 switched to Tuya-supported [tuya-iot-py-sdk](https://pypi.org/project/tuya-iot-py-sdk/) library (finally 😮‍💨), there is no sense to continue supporting this library which uses deprecated API.\n\nCloned from the abandoned package [tuyapy](https://pypi.org/project/tuyapy/) v0.1.3. This package implements a Tuya\nAPI endpoint that was specially designed for Home Assistant.\n\nThis clone contains several critical fixes. Check commits.\n\n## FAQ\n### How to check whether the API this library using can control your device?\n\n- Copy [this script](https://github.com/PaulAnnekov/tuyaha/blob/master/tools/debug_discovery.py) to your PC with Python\n  installed or to https://repl.it/\n- Set/update config inside and run it\n- Check if your devices are listed\n  - If they are and description matches real device (e.g. lamp is lamp, not switch) - device is supported\n  - If they are not or description doesn't match real device - don't open an issue. Ask [Tuya support](mailto:support@tuya.com) to support your device in their \n    `/homeassistant` API\n- Remove the updated script, so your credentials won't leak\n\n### My device is not listed in Tuya API response or contains incomplete state, what should I do?\n\nTry new custom component from Tuya developers https://github.com/tuya/tuya-home-assistant/ or ask them to support your device.\n"
  },
  {
    "path": "setup.py",
    "content": "# coding=utf-8\nimport setuptools\n\nwith open(\"README.md\", \"r\") as fh:\n    long_description = fh.read()\n\nsetuptools.setup(\n    name=\"tuyaha\",\n    version=\"0.0.11\",\n    author=\"Pavlo Annekov and original Tuya authors\",\n    author_email=\"paul.annekov@gmail.com\",\n    description=\"A Python library that implements a Tuya API endpoint that was specially designed for Home Assistant\",\n    long_description=long_description,\n    long_description_content_type=\"text/markdown\",\n    packages=setuptools.find_packages(),\n    url=\"https://github.com/PaulAnnekov/tuyaha\",\n    license=\"MIT\",\n    install_requires=[\"requests\"],\n    classifiers=(\n        \"Programming Language :: Python :: 3\",\n        \"License :: OSI Approved :: MIT License\",\n        \"Operating System :: OS Independent\",\n    ),\n)\n"
  },
  {
    "path": "tools/debug_discovery.py",
    "content": "# The script is intended to get a list of all devices available via Tuya Home Assistant API endpoint.\nimport requests\nimport pprint\n\n# CHANGE THIS - BEGGINING\nUSERNAME = \"\"\nPASSWORD = \"\"\nREGION = \"eu\" # cn, eu, us\nCOUNTRY_CODE = \"1\" # Your account country code, e.g., 1 for USA or 86 for China\nBIZ_TYPE = \"smart_life\" # tuya, smart_life, jinvoo_smart\nFROM = \"tuya\" # you likely don't need to touch this\n# CHANGE THIS - END\n\n# NO NEED TO CHANGE ANYTHING BELOW\nTUYACLOUDURL = \"https://px1.tuya{}.com\"\n\npp = pprint.PrettyPrinter(indent=4)\n\nprint(\"Getting credentials\")\nauth_response = requests.post(\n    (TUYACLOUDURL + \"/homeassistant/auth.do\").format(REGION),\n    data={\n        \"userName\": USERNAME,\n        \"password\": PASSWORD,\n        \"countryCode\": COUNTRY_CODE,\n        \"bizType\": BIZ_TYPE,\n        \"from\": FROM,\n    },\n)\nprint(\"Got credentials\")\nauth_response = auth_response.json()\npp.pprint(auth_response)\n\nheader = {\"name\": \"Discovery\", \"namespace\": \"discovery\", \"payloadVersion\": 1}\npayload = {\"accessToken\": auth_response[\"access_token\"]}\ndata = {\"header\": header, \"payload\": payload}\nprint(\"Getting devices\")\ndiscovery_response = requests.post(\n    (TUYACLOUDURL + \"/homeassistant/skill\").format(REGION), json=data\n)\nprint(\"Got devices\")\ndiscovery_response = discovery_response.json()\npp.pprint(discovery_response)\nprint(\"!!! NOW REMOVE THIS FILE, SO YOUR CREDENTIALS (username, password) WON'T LEAK !!!\")\n"
  },
  {
    "path": "tuyaha/__init__.py",
    "content": "\"\"\"Init file for test\"\"\"\nfrom .tuyaapi import TuyaApi\n"
  },
  {
    "path": "tuyaha/devices/__init__.py",
    "content": ""
  },
  {
    "path": "tuyaha/devices/base.py",
    "content": "import time\nfrom datetime import datetime\n\n\nclass TuyaDevice:\n\n    def __init__(self, data, api):\n        self.api = api\n        self.data = data.get(\"data\")\n        self.obj_id = data.get(\"id\")\n        self.obj_type = data.get(\"ha_type\")\n        self.obj_name = data.get(\"name\")\n        self.dev_type = data.get(\"dev_type\")\n        self.icon = data.get(\"icon\")\n        self._first_update = True\n        self._last_update = datetime.min\n        self._last_query = datetime.min\n\n    def name(self):\n        return self.obj_name\n\n    def state(self):\n        state = self.data.get(\"state\")\n        if state is None:\n            return None\n        elif isinstance(state, str):\n            if state == \"true\":\n                return True\n            return False\n        else:\n            return bool(state)\n\n    def device_type(self):\n        return self.dev_type\n\n    def object_id(self):\n        return self.obj_id\n\n    def object_type(self):\n        return self.obj_type\n\n    def available(self):\n        return self.data.get(\"online\")\n\n    def iconurl(self):\n        return self.icon\n\n    def _update_data(self, key, value, force_val=False):\n        if self.data:\n            # device properties not provided by Tuya API are saved in the\n            # device cache only if force_val=True. This is used to force\n            # in cache missing API values (e.g color mode for light)\n            if not force_val and self.data.get(key) is None:\n                return\n            self.data[key] = value\n            self.api.update_device_data(self.obj_id, self.data)\n\n    def _control_device(self, action, param=None):\n        success, response = self.api.device_control(self.obj_id, action, param)\n        if not success:\n            self._update_data(\"online\", False)\n        else:\n            self._last_update = datetime.now()\n        return success\n\n    # Update device cache using discovery or query command\n    # Due to the limitation of both command it is possible\n    # to choose which one to use. Because discovery return data\n    # for all devices, is preferred method with multiple device\n    # Query can be called with higher frequency but return\n    # values for a single device\n    def _update(self, use_discovery):\n\n        # Avoid get cache value after control.\n        difference = (datetime.now() - self._last_update).total_seconds()\n        wait_delay = difference < 0.5\n\n        data = None\n        if use_discovery or self._first_update:\n            if wait_delay:\n                time.sleep(0.5)\n            # workaround for https://github.com/PaulAnnekov/tuyaha/issues/3\n            self._first_update = False\n            devices = self.api.discovery()\n            if not devices:\n                return\n            for device in devices:\n                if device[\"id\"] == self.obj_id:\n                    data = device[\"data\"]\n                    break\n\n        else:\n            # query can be called once every 60 seconds\n            difference = (datetime.now() - self._last_query).total_seconds()\n            if difference < self.api.query_interval:\n                return\n            if difference == self.api.query_interval:\n                wait_delay = True\n            if wait_delay:\n                time.sleep(0.5)\n\n            try:\n                success, response = self.api.device_control(\n                    self.obj_id, \"QueryDevice\", namespace=\"query\"\n                )\n            finally:\n                self._last_query = datetime.now()\n            if success:\n                data = response[\"payload\"][\"data\"]\n\n        if data:\n            if not self.data:\n                self.data = data\n            else:\n                self.data.update(data)\n            return True\n\n        return\n\n    def __repr__(self):\n        module = self.__class__.__module__\n        if module is None or module == str.__class__.__module__:\n            module = \"\"\n        else:\n            module += \".\"\n        return '<{module}{clazz}: \"{name}\" ({obj_id})>'.format(\n            module=module,\n            clazz=self.__class__.__name__,\n            name=self.obj_name,\n            obj_id=self.obj_id\n        )\n\n    def update(self, use_discovery=True):\n        return self._update(use_discovery)\n"
  },
  {
    "path": "tuyaha/devices/climate.py",
    "content": "from tuyaha.devices.base import TuyaDevice\n\nUNIT_CELSIUS = \"CELSIUS\"\nUNIT_FAHRENHEIT = \"FAHRENHEIT\"\n\nSTEP_WHOLE = 1\nSTEP_HALVES = 0.5\nSTEP_TENTHS = 0.1\n\nTEMP_STEPS = {\n    STEP_WHOLE: \"Whole\",\n    STEP_HALVES: \"Halves\",\n    STEP_TENTHS: \"Tenths\",\n}\n\n\nclass TuyaClimate(TuyaDevice):\n\n    def __init__(self, data, api):\n        super().__init__(data, api)\n        self._unit = None\n        self._divider = 0\n        self._divider_set = False\n        self._ct_divider = 0\n\n    # this function return the temperature value\n    # divided by the _divider attribute previous set\n    # this is required because in same case API provide\n    # a temperature value that must be divided to provide decimal\n    # in other case just return the right temperature values\n    def _set_decimal(self, val, divider=0):\n        if val is None:\n            return None\n        if divider == 0:\n            divider = self._divider\n            if divider == 0:\n                if val > 500 or val < -100:\n                    # in this case we suppose that returned value\n                    # support decimal and must be divided by 100\n                    divider = 100\n                    self._divider = divider\n                else:\n                    divider = 1\n\n        return round(float(val / divider), 2)\n\n    # when unit is not provided by the API or the API return\n    # incorrect value, it can be forced by this method\n    # the _unit attribute is used by the temperature_unit() method\n    def set_unit(self, unit):\n        \"\"\"Set temperature unit (CELSIUS or FAHRENHEIT)\"\"\"\n        if unit != UNIT_CELSIUS and unit != UNIT_FAHRENHEIT:\n            raise ValueError(\n                f\"Unit can only be set to {UNIT_CELSIUS} or {UNIT_FAHRENHEIT}\"\n            )\n        self._unit = unit\n\n    @property\n    def temp_divider(self):\n        return self._divider if self._divider_set else 0\n\n    @temp_divider.setter\n    def temp_divider(self, divider):\n        \"\"\"Set a divider used to calculate returned temperature. Default=0\"\"\"\n        if divider < 0:\n            raise ValueError(\"Temperature divider must be a positive value\")\n        # this check is to avoid that divider is reset from\n        # calculated value when is set to 0\n        if (self._divider_set and divider < 1) or divider >= 1:\n            self._divider = int(divider)\n        self._divider_set = divider >= 1\n\n    @property\n    def curr_temp_divider(self):\n        return self._ct_divider\n\n    @curr_temp_divider.setter\n    def curr_temp_divider(self, divider):\n        \"\"\"Set a divider used to calculate returned current temperature\n           If not defined standard temperature divider is used\"\"\"\n        if divider < 0:\n            raise ValueError(\"Current temperature divider must be a positive value\")\n        self._ct_divider = int(divider)\n\n    def has_decimal(self):\n        \"\"\"Return if temperature values support decimal\"\"\"\n        return self._divider >= 10\n\n    def temperature_unit(self):\n        \"\"\"Return the temperature unit for the device\"\"\"\n        if not self._unit:\n            self._unit = self.data.get(\"temp_unit\", UNIT_CELSIUS)\n        return self._unit\n\n    def current_humidity(self):\n        pass\n\n    def target_humidity(self):\n        pass\n\n    def current_operation(self):\n        return self.data.get(\"mode\")\n\n    def operation_list(self):\n        return self.data.get(\"support_mode\")\n\n    def current_temperature(self):\n        \"\"\"Return current temperature for the device\"\"\"\n        curr_temp = self._set_decimal(\n            self.data.get(\"current_temperature\"), self._ct_divider\n        )\n        # when current temperature is not available, target temperature is returned\n        if curr_temp is None:\n            return self.target_temperature()\n        return curr_temp\n\n    def target_temperature(self):\n        \"\"\"Return target temperature for the device\"\"\"\n        return self._set_decimal(self.data.get(\"temperature\"))\n\n    def target_temperature_step(self):\n        if self.has_decimal():\n            return STEP_HALVES\n        return STEP_WHOLE\n\n    def supported_temperature_steps(self):\n        return TEMP_STEPS\n\n    def current_fan_mode(self):\n        \"\"\"Return the fan setting.\"\"\"\n        fan_speed = self.data.get(\"windspeed\")\n        if fan_speed is None:\n            return None\n        if fan_speed == \"1\":\n            return \"low\"\n        elif fan_speed == \"2\":\n            return \"medium\"\n        elif fan_speed == \"3\":\n            return \"high\"\n        return fan_speed\n\n    def fan_list(self):\n        \"\"\"Return the list of available fan modes.\"\"\"\n        return [\"low\", \"medium\", \"high\"]\n\n    def current_swing_mode(self):\n        \"\"\"Return the fan setting.\"\"\"\n        return None\n\n    def swing_list(self):\n        \"\"\"Return the list of available swing modes.\"\"\"\n        return None\n\n    def min_temp(self):\n        return self._set_decimal(self.data.get(\"min_temper\"))\n\n    def max_temp(self):\n        return self._set_decimal(self.data.get(\"max_temper\"))\n\n    def min_humidity(self):\n        pass\n\n    def max_humidity(self):\n        pass\n\n    def set_temperature(self, temperature, use_divider=True):\n        \"\"\"Set new target temperature.\"\"\"\n\n        # the value used to set temperature is scaled based on the configured divider\n        divider = self._divider or 1\n        input_val = float(temperature)\n        scaled_val = input_val * divider\n        digits1 = None if input_val.is_integer() else 1\n        digits2 = None if scaled_val.is_integer() else 1\n\n        set_val = round(scaled_val, digits2)\n        if use_divider:\n            temp_val = round(input_val, digits1)\n        else:\n            temp_val = set_val\n\n        if self._control_device(\"temperatureSet\", {\"value\": temp_val}):\n            self._update_data(\"temperature\", set_val)\n\n    def set_humidity(self, humidity):\n        \"\"\"Set new target humidity.\"\"\"\n        raise NotImplementedError()\n\n    def set_fan_mode(self, fan_mode):\n        \"\"\"Set new target fan mode.\"\"\"\n        if self._control_device(\"windSpeedSet\", {\"value\": fan_mode}):\n            fanList = self.fan_list()\n            if fan_mode in fanList:\n                val = str(fanList.index(fan_mode) + 1)\n            else:\n                val = fan_mode\n            self._update_data(\"windspeed\", val)\n\n    def set_operation_mode(self, operation_mode):\n        \"\"\"Set new target operation mode.\"\"\"\n        if self._control_device(\"modeSet\", {\"value\": operation_mode}):\n            self._update_data(\"mode\", operation_mode)\n\n    def set_swing_mode(self, swing_mode):\n        \"\"\"Set new target swing operation.\"\"\"\n        raise NotImplementedError()\n\n    def support_target_temperature(self):\n        if self.data.get(\"temperature\") is not None:\n            return True\n        else:\n            return False\n\n    def support_mode(self):\n        if self.data.get(\"mode\") is not None:\n            return True\n        else:\n            return False\n\n    def support_wind_speed(self):\n        if self.data.get(\"windspeed\") is not None:\n            return True\n        else:\n            return False\n\n    def support_humidity(self):\n        if self.data.get(\"humidity\") is not None:\n            return True\n        else:\n            return False\n\n    def turn_on(self):\n        if self._control_device(\"turnOnOff\", {\"value\": \"1\"}):\n            self._update_data(\"state\", \"true\")\n\n    def turn_off(self):\n        if self._control_device(\"turnOnOff\", {\"value\": \"0\"}):\n            self._update_data(\"state\", \"false\")\n"
  },
  {
    "path": "tuyaha/devices/cover.py",
    "content": "from tuyaha.devices.base import TuyaDevice\n\n\nclass TuyaCover(TuyaDevice):\n\n    def state(self):\n        state = self.data.get(\"state\")\n        return state\n\n    def open_cover(self):\n        \"\"\"Open the cover.\"\"\"\n        if self._control_device(\"turnOnOff\", {\"value\": \"1\"}):\n            self._update_data(\"state\", 1)\n\n    def close_cover(self):\n        \"\"\"Close cover.\"\"\"\n        if self._control_device(\"turnOnOff\", {\"value\": \"0\"}):\n            self._update_data(\"state\", 2)\n\n    def stop_cover(self):\n        \"\"\"Stop the cover.\"\"\"\n        if self._control_device(\"startStop\", {\"value\": \"0\"}):\n            self._update_data(\"state\", 3)\n\n    def support_stop(self):\n        support = self.data.get(\"support_stop\")\n        if support is None:\n            return False\n        return support\n"
  },
  {
    "path": "tuyaha/devices/factory.py",
    "content": "from tuyaha.devices.climate import TuyaClimate\nfrom tuyaha.devices.cover import TuyaCover\nfrom tuyaha.devices.fan import TuyaFanDevice\nfrom tuyaha.devices.light import TuyaLight\nfrom tuyaha.devices.lock import TuyaLock\nfrom tuyaha.devices.scene import TuyaScene\nfrom tuyaha.devices.switch import TuyaSwitch\n\n\ndef get_tuya_device(data, api):\n    dev_type = data.get(\"dev_type\")\n    devices = []\n\n    if dev_type == \"light\":\n        devices.append(TuyaLight(data, api))\n    elif dev_type == \"climate\":\n        devices.append(TuyaClimate(data, api))\n    elif dev_type == \"scene\":\n        devices.append(TuyaScene(data, api))\n    elif dev_type == \"fan\":\n        devices.append(TuyaFanDevice(data, api))\n    elif dev_type == \"cover\":\n        devices.append(TuyaCover(data, api))\n    elif dev_type == \"lock\":\n        devices.append(TuyaLock(data, api))\n    elif dev_type == \"switch\":\n        devices.append(TuyaSwitch(data, api))\n    return devices\n"
  },
  {
    "path": "tuyaha/devices/fan.py",
    "content": "from tuyaha.devices.base import TuyaDevice\n\n\nclass TuyaFanDevice(TuyaDevice):\n\n    def speed(self):\n        return self.data.get(\"speed\")\n\n    def speed_list(self):\n        speed_list = []\n        speed_level = self.data.get(\"speed_level\")\n        for i in range(speed_level):\n            speed_list.append(str(i + 1))\n        return speed_list\n\n    def oscillating(self):\n        return self.data.get(\"direction\")\n\n    def set_speed(self, speed):\n        if self._control_device(\"windSpeedSet\", {\"value\": speed}):\n            self._update_data(\"speed\", speed)\n\n    def oscillate(self, oscillating):\n        if oscillating:\n            command = \"swingOpen\"\n        else:\n            command = \"swingClose\"\n        if self._control_device(command):\n            self._update_data(\"direction\", oscillating)\n\n    def turn_on(self):\n        if self._control_device(\"turnOnOff\", {\"value\": \"1\"}):\n            self._update_data(\"state\", \"true\")\n\n    def turn_off(self):\n        if self._control_device(\"turnOnOff\", {\"value\": \"0\"}):\n            self._update_data(\"state\", \"false\")\n\n    def support_oscillate(self):\n        if self.oscillating() is None:\n            return False\n        else:\n            return True\n\n    def support_direction(self):\n        return False\n"
  },
  {
    "path": "tuyaha/devices/light.py",
    "content": "from tuyaha.devices.base import TuyaDevice\n\n# The minimum brightness value set in the API that does not turn off the light\nMIN_BRIGHTNESS = 10.3\n\n# the default range used to return brightness\nBRIGHTNESS_STD_RANGE = (1, 255)\n\n# the default range used to set color temperature\nCOLTEMP_SET_RANGE = (1000, 10000)\n\n# the default range used to return color temperature (in kelvin)\nCOLTEMP_KELV_RANGE = (2700, 6500)\n\n\nclass TuyaLight(TuyaDevice):\n\n    def __init__(self, data, api):\n        super().__init__(data, api)\n        self._support_color = False\n        self.brightness_white_range = BRIGHTNESS_STD_RANGE\n        self.brightness_color_range = BRIGHTNESS_STD_RANGE\n        self.color_temp_range = COLTEMP_SET_RANGE\n\n    # if color support is not reported by API can be forced by this method\n    # the attribute _support_color is used by method support_color()\n    def force_support_color(self):\n        self._support_color = True\n\n    def _color_mode(self):\n        work_mode = self.data.get(\"color_mode\", \"white\")\n        return True if work_mode == \"colour\" else False\n\n    @staticmethod\n    def _scale(val, src, dst):\n        \"\"\"Scale the given value from the scale of src to the scale of dst.\"\"\"\n        if val < 0:\n            return dst[0]\n        return ((val - src[0]) / (src[1] - src[0])) * (dst[1] - dst[0]) + dst[0]\n\n    def brightness(self):\n        \"\"\"Return the brightness based on the light status scaled to standard range\"\"\"\n        brightness = -1\n        if self._color_mode():\n            if \"color\" in self.data:\n                brightness = int(self.data.get(\"color\").get(\"brightness\", \"-1\"))\n        else:\n            brightness = int(self.data.get(\"brightness\", \"-1\"))\n        # returned value is scaled using standard range\n        ret_val = TuyaLight._scale(\n            brightness,\n            self._brightness_range(),\n            BRIGHTNESS_STD_RANGE,\n        )\n        return round(ret_val)\n\n    def _set_brightness(self, brightness):\n        if self._color_mode():\n            data = self.data.get(\"color\", {})\n            data[\"brightness\"] = brightness\n            self._update_data(\"color\", data, force_val=True)\n        else:\n            self._update_data(\"brightness\", brightness)\n\n    def _brightness_range(self):\n        \"\"\"return the configured brightness range based on the light status\"\"\"\n        if self._color_mode():\n            return self.brightness_color_range\n        else:\n            return self.brightness_white_range\n\n    def support_color(self):\n        \"\"\"return if the light support color\"\"\"\n        if not self._support_color:\n            if self.data.get(\"color\") or self.data.get(\"color_mode\") == \"colour\":\n                self._support_color = True\n        return self._support_color\n\n    def support_color_temp(self):\n        \"\"\"return if the light support color temperature\"\"\"\n        return self.data.get(\"color_temp\") is not None\n\n    def hs_color(self):\n        \"\"\"return current hs color\"\"\"\n        if self.support_color():\n            color = self.data.get(\"color\")\n            if self._color_mode() and color:\n                return color.get(\"hue\", 0.0), float(color.get(\"saturation\", 0.0)) * 100\n            else:\n                return 0.0, 0.0\n        else:\n            return None\n\n    def color_temp(self):\n        \"\"\"return current color temperature scaled with standard kelvin range\"\"\"\n        temp = self.data.get(\"color_temp\")\n        # convert color temperature to kelvin scale for returned value\n        ret_value = TuyaLight._scale(\n            temp,\n            self.color_temp_range,\n            COLTEMP_KELV_RANGE,\n        )\n        return round(ret_value)\n\n    def min_color_temp(self):\n        return COLTEMP_KELV_RANGE[1]\n\n    def max_color_temp(self):\n        return COLTEMP_KELV_RANGE[0]\n\n    def turn_on(self):\n        if self._control_device(\"turnOnOff\", {\"value\": \"1\"}):\n            self._update_data(\"state\", \"true\")\n\n    def turn_off(self):\n        if self._control_device(\"turnOnOff\", {\"value\": \"0\"}):\n            self._update_data(\"state\", \"false\")\n\n    def set_brightness(self, brightness):\n        \"\"\"Set the brightness(0-255) of light.\"\"\"\n        if int(brightness) > 0:\n            # convert to scale 0-100 with MIN_BRIGHTNESS to set the value\n            set_value = TuyaLight._scale(\n                brightness,\n                BRIGHTNESS_STD_RANGE,\n                (MIN_BRIGHTNESS, 100),\n            )\n            if self._control_device(\"brightnessSet\", {\"value\": round(set_value, 1)}):\n                self._update_data(\"state\", \"true\")\n                # convert to scale configured for brightness range to update the cache\n                value = TuyaLight._scale(\n                    brightness,\n                    BRIGHTNESS_STD_RANGE,\n                    self._brightness_range(),\n                )\n                self._set_brightness(round(value))\n        else:\n            self.turn_off()\n\n    def set_color(self, color):\n        \"\"\"Set the color of light.\"\"\"\n        cur_brightness = self.data.get(\"color\", {}).get(\n            \"brightness\", self.brightness_color_range[0]\n        )\n        hsv_color = {\n            \"hue\": color[0] if color[1] != 0 else 0,  # color white\n            \"saturation\": color[1] / 100,\n        }\n        if len(color) < 3:\n            hsv_color[\"brightness\"] = cur_brightness\n        else:\n            hsv_color[\"brightness\"] = color[2]\n        # color white\n        white_mode = hsv_color[\"saturation\"] == 0\n        is_color = self._color_mode()\n        if self._control_device(\"colorSet\", {\"color\": hsv_color}):\n            self._update_data(\"state\", \"true\")\n            self._update_data(\"color\", hsv_color, force_val=True)\n            if not is_color and not white_mode:\n                self._update_data(\"color_mode\", \"colour\")\n            elif is_color and white_mode:\n                self._update_data(\"color_mode\", \"white\")\n\n    def set_color_temp(self, color_temp):\n        \"\"\"Set the color temperature of light.\"\"\"\n        # convert to scale configured for color temperature to update the value\n        set_value = TuyaLight._scale(\n            color_temp,\n            COLTEMP_KELV_RANGE,\n            COLTEMP_SET_RANGE,\n        )\n        if self._control_device(\"colorTemperatureSet\", {\"value\": round(set_value)}):\n            self._update_data(\"state\", \"true\")\n            self._update_data(\"color_mode\", \"white\")\n            # convert to scale configured for color temperature to update the cache\n            data_value = TuyaLight._scale(\n                color_temp,\n                COLTEMP_KELV_RANGE,\n                self.color_temp_range,\n            )\n            self._update_data(\"color_temp\", round(data_value))\n"
  },
  {
    "path": "tuyaha/devices/lock.py",
    "content": "from tuyaha.devices.base import TuyaDevice\n\n\nclass TuyaLock(TuyaDevice):\n    def state(self):\n        state = self.data.get(\"state\")\n        if state == \"true\":\n            return True\n        elif state == \"false\":\n            return False\n        else:\n            return None\n"
  },
  {
    "path": "tuyaha/devices/remote.py",
    "content": ""
  },
  {
    "path": "tuyaha/devices/scene.py",
    "content": "from tuyaha.devices.base import TuyaDevice\n\n\nclass TuyaScene(TuyaDevice):\n    def available(self):\n        return True\n\n    def activate(self):\n        self.api.device_control(self.obj_id, \"turnOnOff\", {\"value\": \"1\"})\n\n    def update(self, use_discovery=True):\n        return True\n"
  },
  {
    "path": "tuyaha/devices/switch.py",
    "content": "\nfrom tuyaha.devices.base import TuyaDevice\n\n\nclass TuyaSwitch(TuyaDevice):\n\n    def turn_on(self):\n        if self._control_device(\"turnOnOff\", {\"value\": \"1\"}):\n            self._update_data(\"state\", True)\n\n    def turn_off(self):\n        if self._control_device(\"turnOnOff\", {\"value\": \"0\"}):\n            self._update_data(\"state\", False)\n\n    def update(self, use_discovery=True):\n        return self._update(use_discovery=True)\n"
  },
  {
    "path": "tuyaha/tuyaapi.py",
    "content": "import logging\nimport time\n\nimport requests\nfrom datetime import datetime\nfrom requests.exceptions import ConnectionError as RequestsConnectionError\nfrom requests.exceptions import HTTPError as RequestsHTTPError\nfrom threading import Lock\n\nfrom tuyaha.devices.factory import get_tuya_device\n\nTUYACLOUDURL = \"https://px1.tuya{}.com\"\nDEFAULTREGION = \"us\"\n\n# Tuya API do not allow call to discovery command below specific limits\n# Use discovery_interval property to set correct value based on API discovery limits\n# Next 2 parameter define the default and minimum allowed value for the property\nMIN_DISCOVERY_INTERVAL = 10.0\nDEF_DISCOVERY_INTERVAL = 60.0\n\n# Tuya API do not allow call to query command below specific limits\n# Use query_interval property to set correct value based on API query limits\n# Next 2 parameter define the default and minimum allowed value for the property\nMIN_QUERY_INTERVAL = 10.0\nDEF_QUERY_INTERVAL = 30.0\n\nREFRESHTIME = 60 * 60 * 12\n\n_LOGGER = logging.getLogger(__name__)\nlock = Lock()\n\n\nclass TuyaSession:\n\n    username = \"\"\n    password = \"\"\n    countryCode = \"\"\n    bizType = \"\"\n    accessToken = \"\"\n    refreshToken = \"\"\n    expireTime = 0\n    devices = []\n    region = DEFAULTREGION\n\n\nSESSION = TuyaSession()\n\n\nclass TuyaApi:\n\n    def __init__(self):\n        self._requestSession = None\n        self._discovered_devices = None\n        self._last_discovery = None\n        self._force_discovery = False\n        self._discovery_interval = DEF_DISCOVERY_INTERVAL\n        self._query_interval = DEF_QUERY_INTERVAL\n        self._discovery_fail_count = 0\n\n    @property\n    def discovery_interval(self):\n        \"\"\"The interval in seconds between 2 consecutive device discovery\"\"\"\n        return self._discovery_interval\n\n    @discovery_interval.setter\n    def discovery_interval(self, val):\n        if val < MIN_DISCOVERY_INTERVAL:\n            raise ValueError(\n                f\"Discovery interval below {MIN_DISCOVERY_INTERVAL} seconds is invalid\"\n            )\n        self._discovery_interval = val\n\n    @property\n    def query_interval(self):\n        \"\"\"The interval in seconds between 2 consecutive device query\"\"\"\n        return self._query_interval\n\n    @query_interval.setter\n    def query_interval(self, val):\n        if val < MIN_QUERY_INTERVAL:\n            raise ValueError(\n                f\"Query interval below {MIN_QUERY_INTERVAL} seconds is invalid\"\n            )\n        self._query_interval = val\n\n    def init(self, username, password, countryCode, bizType=\"\", region=DEFAULTREGION):\n        SESSION.username = username\n        SESSION.password = password\n        SESSION.countryCode = countryCode\n        SESSION.bizType = bizType\n        SESSION.region = region\n\n        self._requestSession = requests.Session()\n\n        if username is None or password is None:\n            return None\n        else:\n            self.get_access_token()\n            self.discover_devices()\n            return SESSION.devices\n\n    def get_access_token(self):\n        try:\n            response = self._requestSession.post(\n                (TUYACLOUDURL + \"/homeassistant/auth.do\").format(SESSION.region),\n                data={\n                    \"userName\": SESSION.username,\n                    \"password\": SESSION.password,\n                    \"countryCode\": SESSION.countryCode,\n                    \"bizType\": SESSION.bizType,\n                    \"from\": \"tuya\",\n                },\n            )\n            response.raise_for_status()\n        except RequestsConnectionError as ex:\n            raise TuyaNetException from ex\n        except RequestsHTTPError as ex:\n            if response.status_code >= 500:\n                raise TuyaServerException from ex\n\n        response_json = response.json()\n        if response_json.get(\"responseStatus\") == \"error\":\n            message = response_json.get(\"errorMsg\")\n            if message == \"error\":\n                raise TuyaAPIException(\"get access token failed\")\n            elif message == \"you cannot auth exceed once in 60 seconds\":\n                raise TuyaAPIRateLimitException(\"login rate limited\")\n            else:\n                raise TuyaAPIException(message)\n\n        SESSION.accessToken = response_json.get(\"access_token\")\n        SESSION.refreshToken = response_json.get(\"refresh_token\")\n        SESSION.expireTime = int(time.time()) + response_json.get(\"expires_in\")\n        areaCode = SESSION.accessToken[0:2]\n        if areaCode == \"AY\":\n            SESSION.region = \"cn\"\n        elif areaCode == \"EU\":\n            SESSION.region = \"eu\"\n        else:\n            SESSION.region = \"us\"\n\n    def check_access_token(self):\n        if SESSION.username == \"\" or SESSION.password == \"\":\n            raise TuyaAPIException(\"can not find username or password\")\n        if SESSION.accessToken == \"\" or SESSION.refreshToken == \"\":\n            self.get_access_token()\n            self._force_discovery = True\n        elif SESSION.expireTime <= REFRESHTIME + int(time.time()):\n            self.refresh_access_token()\n            self._force_discovery = True\n\n    def refresh_access_token(self):\n        data = \"grant_type=refresh_token&refresh_token=\" + SESSION.refreshToken\n        response = self._requestSession.get(\n            (TUYACLOUDURL + \"/homeassistant/access.do\").format(SESSION.region)\n            + \"?\"\n            + data\n        )\n        response_json = response.json()\n        if response_json.get(\"responseStatus\") == \"error\":\n            raise TuyaAPIException(\"refresh token failed\")\n\n        SESSION.accessToken = response_json.get(\"access_token\")\n        SESSION.refreshToken = response_json.get(\"refresh_token\")\n        SESSION.expireTime = int(time.time()) + response_json.get(\"expires_in\")\n\n    def poll_devices_update(self):\n        self.check_access_token()\n        return self.discover_devices()\n\n    def update_device_data(self, dev_id, data):\n        for device in self._discovered_devices:\n            if device[\"id\"] == dev_id:\n                device[\"data\"] = data\n\n    def _call_discovery(self):\n        if not self._last_discovery or self._force_discovery:\n            self._force_discovery = False\n            return True\n        difference = (datetime.now() - self._last_discovery).total_seconds()\n        if difference > self.discovery_interval:\n            return True\n        return False\n\n    # if discovery is called before that configured polling interval has passed\n    # it return cached data retrieved by previous successful call\n    def discovery(self):\n        with lock:\n            if self._call_discovery():\n                try:\n                    response = self._request(\"Discovery\", \"discovery\")\n                finally:\n                    self._last_discovery = datetime.now()\n                if response:\n                    result_code = response[\"header\"][\"code\"]\n                    if result_code == \"SUCCESS\":\n                        self._discovery_fail_count = 0\n                        self._discovered_devices = response[\"payload\"][\"devices\"]\n                        self._load_session_devices()\n            else:\n                _LOGGER.debug(\"Discovery: Use cached info\")\n        return self._discovered_devices\n\n    def _load_session_devices(self):\n        SESSION.devices = []\n        for device in self._discovered_devices:\n            SESSION.devices.extend(get_tuya_device(device, self))\n\n    def discover_devices(self):\n        devices = self.discovery()\n        if not devices:\n            return None\n        return devices\n\n    def get_devices_by_type(self, dev_type):\n        device_list = []\n        for device in SESSION.devices:\n            if device.device_type() == dev_type:\n                device_list.append(device)\n        return device_list\n\n    def get_all_devices(self):\n        return SESSION.devices\n\n    def get_device_by_id(self, dev_id):\n        for device in SESSION.devices:\n            if device.object_id() == dev_id:\n                return device\n        return None\n\n    def device_control(self, devId, action, param=None, namespace=\"control\"):\n        if param is None:\n            param = {}\n        response = self._request(action, namespace, devId, param)\n        if response and response[\"header\"][\"code\"] == \"SUCCESS\":\n            success = True\n        else:\n            success = False\n        return success, response\n\n    def _request(self, name, namespace, devId=None, payload={}):\n        header = {\"name\": name, \"namespace\": namespace, \"payloadVersion\": 1}\n        payload[\"accessToken\"] = SESSION.accessToken\n        if namespace != \"discovery\":\n            payload[\"devId\"] = devId\n        data = {\"header\": header, \"payload\": payload}\n        try:\n            response = self._requestSession.post(\n                (TUYACLOUDURL + \"/homeassistant/skill\").format(SESSION.region), json=data\n            )\n        except RequestsConnectionError as ex:\n            _LOGGER.warning(\n                \"request error, error code is %s, device %s\",\n                ex,\n                devId,\n            )\n            return\n\n        if not response.ok:\n            _LOGGER.warning(\n                \"request error, status code is %d, device %s\",\n                response.status_code,\n                devId,\n            )\n            return\n        response_json = response.json()\n        result_code = response_json[\"header\"][\"code\"]\n        if result_code != \"SUCCESS\":\n            if result_code == \"FrequentlyInvoke\":\n                self._raise_frequently_invoke(\n                    name, response_json[\"header\"].get(\"msg\", result_code), devId\n                )\n            else:\n                _LOGGER.warning(\n                    \"control device error, error code is \" + response_json[\"header\"][\"code\"]\n                )\n        return response_json\n\n    def _raise_frequently_invoke(self, action, error_msg, dev_id):\n        if action == \"Discovery\":\n            self._discovery_fail_count += 1\n            text = (\n                \"Method [Discovery] fails {} time(s) using poll interval {} - error: {}\"\n            )\n            message = text.format(\n                self._discovery_fail_count, self.discovery_interval, error_msg\n            )\n        else:\n            text = \"Method [{}] for device {} fails {}- error: {}\"\n            msg_interval = \"\"\n            if action == \"QueryDevice\":\n                msg_interval = \"using poll interval {} \".format(self.query_interval)\n            message = text.format(action, dev_id, msg_interval, error_msg)\n\n        raise TuyaFrequentlyInvokeException(message)\n\n\nclass TuyaAPIException(Exception):\n    pass\n\n\nclass TuyaNetException(Exception):\n    pass\n\n\nclass TuyaServerException(Exception):\n    pass\n\n\nclass TuyaFrequentlyInvokeException(Exception):\n    pass\n\n\nclass TuyaAPIRateLimitException(Exception):\n    pass\n"
  }
]