├── custom_components └── xiaomi_mijia_ptx │ ├── __init__.py │ ├── const.py │ ├── manifest.json │ ├── switch.py │ └── ptxswitch.py ├── LICENSE ├── info.md ├── README.md └── .gitignore /custom_components/xiaomi_mijia_ptx/__init__.py: -------------------------------------------------------------------------------- 1 | """Support for Xiaomi Miio PTX Touch Switch.""" 2 | -------------------------------------------------------------------------------- /custom_components/xiaomi_mijia_ptx/const.py: -------------------------------------------------------------------------------- 1 | """Constants for the Xiaomi Miio component.""" 2 | DOMAIN = "xiaomi_mijia_ptx" 3 | 4 | -------------------------------------------------------------------------------- /custom_components/xiaomi_mijia_ptx/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "xiaomi_mijia_ptx", 3 | "name": "Xiaomi mijia PTX", 4 | "documentation": "https://github.com/volshebniks/xiaomi_mijia_ptx", 5 | "requirements": [], 6 | "dependencies": [], 7 | "version": "0.3.1", 8 | "codeowners": [ 9 | "@volshebniks" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Sergey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /info.md: -------------------------------------------------------------------------------- 1 | # xiaomi_mijia_ptx 2 | Support for Xiaomi Miio PTX Touch Switch 3 | 4 | [![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/custom-components/hacs) 5 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/volshebniks/xiaomi_mijia_ptx)](https://github.com/volshebniks/xiaomi_mijia_ptx/releases) 6 | ![GitHub Release Date](https://img.shields.io/github/release-date/volshebniks/xiaomi_mijia_ptx) 7 | [![GitHub](https://img.shields.io/github/license/volshebniks/xiaomi_mijia_ptx)](LICENSE) 8 | 9 | [![Maintenance](https://img.shields.io/badge/Maintained%3F-Yes-brightgreen.svg)](https://github.com/volshebniks/xiaomi_mijia_ptx/graphs/commit-activity) 10 | [![GitHub issues](https://img.shields.io/github/issues/volshebniks/xiaomi_mijia_ptx)](https://github.com/volshebniks/xiaomi_mijia_ptx/issues) 11 | 12 | [![Donate](https://img.shields.io/badge/donate-Coffee-yellow.svg)](https://https://www.buymeacoffee.com/RlnBV9r) 13 | [![Donate](https://img.shields.io/badge/donate-Yandex-red.svg)](https://money.yandex.ru/to/41001566881198) 14 | 15 | The 'xiaomi_mijia_ptx' component is a Home Assistant custom switch for Support for Xiaomi Miio PTX Touch Switch. 16 | 17 | ## Table of Contents 18 | 19 | * [Installation](#installation) 20 | * [Manual Installation](#manual-installation) 21 | * [Installation via HACS](#installation-via-hacs) 22 | * [Configuration](#configuration) 23 | * [Configuration Parameters](#configuration-parameters) 24 | * [State and Attributes](#state-and-attributes) 25 | * [State](#state) 26 | * [Attributes](#attributes) 27 | * [Notes about unit of measurement](#notes-about-unit-of-measurement) 28 | 29 | ## Installation 30 | 31 | ### MANUAL INSTALLATION 32 | 33 | 1. Download the `xiaomi_mijia_ptx.zip` file from the 34 | [latest release](https://github.com/volshebniks/xiaomi_mijia_ptx/releases/latest). 35 | 2. Unpack the release and copy the `custom_components/xiaomi_mijia_ptx/` directory 36 | into the `custom_components` directory of your Home Assistant 37 | installation. 38 | 3. Configure the `xiaomi_mijia_ptx` switch. 39 | 4. Restart Home Assistant. 40 | 41 | ### INSTALLATION VIA HACS 42 | 43 | 1. Ensure that [HACS](https://custom-components.github.io/hacs/) is installed. 44 | 2. Add https://github.com/volshebniks/xiaomi_mijia_ptx ti custom repositiries 45 | 3. Configure the `xiaomi_mijia_ptx` switch. 46 | 4. Restart Home Assistant. 47 | 48 | ## Configuration 49 | 50 | xiaomi_mijia_ptx can be configured in configuration.yaml 51 | 52 | 53 | ### configuration.yaml 54 | 55 | Add `xiaomi_mijia_ptx` switch in your `configuration.yaml`. 56 | 57 | ```yaml 58 | # Example configuration.yaml entry 59 | 60 | switch: 61 | - platform: xiaomi_mijia_ptx 62 | host: YOUR_DEVICE_IP_ADRESS 63 | token: YOUR_DEVICE_TOKEN 64 | model: 090615.switch.switch01 65 | name: xiaomi_smart_switch 66 | ``` 67 | 68 | ### CONFIGURATION PARAMETERS 69 | 70 | |Attribute |Optional|Description 71 | |:----------|----------|------------ 72 | | `host` | No | Your switch ip address 73 | |`token` | No | Your switch secret token 74 | | `model` | No | May be '090615.switch.switch01' or '090615.switch.switch02' or '090615.switch.switch03' 75 | | `name` | No | Name switch 76 | 77 | 78 | Tested with 1 key and 3 keys switch 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xiaomi_mijia_ptx 2 | Support for Xiaomi Miio PTX Touch Switch 3 | 4 | [![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/custom-components/hacs) 5 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/volshebniks/xiaomi_mijia_ptx)](https://github.com/volshebniks/xiaomi_mijia_ptx/releases) 6 | ![GitHub Release Date](https://img.shields.io/github/release-date/volshebniks/xiaomi_mijia_ptx) 7 | [![GitHub](https://img.shields.io/github/license/volshebniks/xiaomi_mijia_ptx)](LICENSE) 8 | 9 | [![Maintenance](https://img.shields.io/badge/Maintained%3F-Yes-brightgreen.svg)](https://github.com/volshebniks/xiaomi_mijia_ptx/graphs/commit-activity) 10 | [![GitHub issues](https://img.shields.io/github/issues/volshebniks/xiaomi_mijia_ptx)](https://github.com/volshebniks/xiaomi_mijia_ptx/issues) 11 | 12 | [![Donate](https://img.shields.io/badge/donate-Coffee-yellow.svg)](https://https://www.buymeacoffee.com/RlnBV9r) 13 | [![Donate](https://img.shields.io/badge/donate-Yandex-red.svg)](https://money.yandex.ru/to/41001566881198) 14 | 15 | 16 | The 'xiaomi_mijia_ptx' component is a Home Assistant custom switch for Support for Xiaomi Miio PTX Touch Switch. 17 | 18 | ## Table of Contents 19 | 20 | * [Installation](#installation) 21 | * [Manual Installation](#manual-installation) 22 | * [Installation via HACS](#installation-via-hacs) 23 | * [Configuration](#configuration) 24 | * [Configuration Parameters](#configuration-parameters) 25 | * [State and Attributes](#state-and-attributes) 26 | * [State](#state) 27 | * [Attributes](#attributes) 28 | * [Notes about unit of measurement](#notes-about-unit-of-measurement) 29 | 30 | ## Installation 31 | 32 | ### MANUAL INSTALLATION 33 | 34 | 1. Download the `xiaomi_mijia_ptx.zip` file from the 35 | [latest release](https://github.com/volshebniks/xiaomi_mijia_ptx/releases/latest). 36 | 2. Unpack the release and copy the `custom_components/xiaomi_mijia_ptx/` directory 37 | into the `custom_components` directory of your Home Assistant 38 | installation. 39 | 3. Configure the `xiaomi_mijia_ptx` switch. 40 | 4. Restart Home Assistant. 41 | 42 | ### INSTALLATION VIA HACS 43 | 44 | 1. Ensure that [HACS](https://custom-components.github.io/hacs/) is installed. 45 | 2. Add https://github.com/volshebniks/xiaomi_mijia_ptx ti custom repositiries 46 | 3. Configure the `xiaomi_mijia_ptx` switch. 47 | 4. Restart Home Assistant. 48 | 49 | ## Configuration 50 | 51 | xiaomi_mijia_ptx can be configured in configuration.yaml 52 | 53 | 54 | ### configuration.yaml 55 | 56 | Add `xiaomi_mijia_ptx` switch in your `configuration.yaml`. 57 | 58 | ```yaml 59 | # Example configuration.yaml entry 60 | 61 | switch: 62 | - platform: xiaomi_mijia_ptx 63 | host: YOUR_DEVICE_IP_ADRESS 64 | token: YOUR_DEVICE_TOKEN 65 | model: 090615.switch.switch01 66 | name: xiaomi_smart_switch 67 | ``` 68 | 69 | ### CONFIGURATION PARAMETERS 70 | 71 | |Attribute |Optional|Description 72 | |:----------|----------|------------ 73 | | `host` | No | Your switch ip address 74 | |`token` | No | Your switch secret token 75 | | `model` | No | May be '090615.switch.switch01' or '090615.switch.switch02' or '090615.switch.switch03' or '090615.switch.xswitch01' or '090615.switch.xswitch02' or '090615.switch.xswitch03' 76 | | `name` | No | Name switch 77 | 78 | 79 | Tested with 1 key and 3 keys switch 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | *.pyproj 10 | *.sln 11 | *.pyproj.user 12 | *.7z 13 | 14 | # User-specific files (MonoDevelop/Xamarin Studio) 15 | *.userprefs 16 | 17 | # Build results 18 | [Dd]ebug/ 19 | [Dd]ebugPublic/ 20 | [Rr]elease/ 21 | [Rr]eleases/ 22 | x64/ 23 | x86/ 24 | bld/ 25 | [Bb]in/ 26 | [Oo]bj/ 27 | [Ll]og/ 28 | 29 | # Visual Studio 2015 cache/options directory 30 | .vs/ 31 | # Uncomment if you have tasks that create the project's static files in wwwroot 32 | #wwwroot/ 33 | 34 | # MSTest test Results 35 | [Tt]est[Rr]esult*/ 36 | [Bb]uild[Ll]og.* 37 | 38 | # NUNIT 39 | *.VisualState.xml 40 | TestResult.xml 41 | 42 | # Build Results of an ATL Project 43 | [Dd]ebugPS/ 44 | [Rr]eleasePS/ 45 | dlldata.c 46 | 47 | # DNX 48 | project.lock.json 49 | project.fragment.lock.json 50 | artifacts/ 51 | 52 | *_i.c 53 | *_p.c 54 | *_i.h 55 | *.ilk 56 | *.meta 57 | *.obj 58 | *.pch 59 | *.pdb 60 | *.pgc 61 | *.pgd 62 | *.rsp 63 | *.sbr 64 | *.tlb 65 | *.tli 66 | *.tlh 67 | *.tmp 68 | *.tmp_proj 69 | *.log 70 | *.vspscc 71 | *.vssscc 72 | .builds 73 | *.pidb 74 | *.svclog 75 | *.scc 76 | 77 | # Chutzpah Test files 78 | _Chutzpah* 79 | 80 | # Visual C++ cache files 81 | ipch/ 82 | *.aps 83 | *.ncb 84 | *.opendb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | *.VC.db 89 | *.VC.VC.opendb 90 | 91 | # Visual Studio profiler 92 | *.psess 93 | *.vsp 94 | *.vspx 95 | *.sap 96 | 97 | # TFS 2012 Local Workspace 98 | $tf/ 99 | 100 | # Guidance Automation Toolkit 101 | *.gpState 102 | 103 | # ReSharper is a .NET coding add-in 104 | _ReSharper*/ 105 | *.[Rr]e[Ss]harper 106 | *.DotSettings.user 107 | 108 | # JustCode is a .NET coding add-in 109 | .JustCode 110 | 111 | # TeamCity is a build add-in 112 | _TeamCity* 113 | 114 | # DotCover is a Code Coverage Tool 115 | *.dotCover 116 | 117 | # NCrunch 118 | _NCrunch_* 119 | .*crunch*.local.xml 120 | nCrunchTemp_* 121 | 122 | # MightyMoose 123 | *.mm.* 124 | AutoTest.Net/ 125 | 126 | # Web workbench (sass) 127 | .sass-cache/ 128 | 129 | # Installshield output folder 130 | [Ee]xpress/ 131 | 132 | # DocProject is a documentation generator add-in 133 | DocProject/buildhelp/ 134 | DocProject/Help/*.HxT 135 | DocProject/Help/*.HxC 136 | DocProject/Help/*.hhc 137 | DocProject/Help/*.hhk 138 | DocProject/Help/*.hhp 139 | DocProject/Help/Html2 140 | DocProject/Help/html 141 | 142 | # Click-Once directory 143 | publish/ 144 | 145 | # Publish Web Output 146 | *.[Pp]ublish.xml 147 | *.azurePubxml 148 | # TODO: Comment the next line if you want to checkin your web deploy settings 149 | # but database connection strings (with potential passwords) will be unencrypted 150 | #*.pubxml 151 | *.publishproj 152 | 153 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 154 | # checkin your Azure Web App publish settings, but sensitive information contained 155 | # in these scripts will be unencrypted 156 | PublishScripts/ 157 | 158 | # NuGet Packages 159 | *.nupkg 160 | # The packages folder can be ignored because of Package Restore 161 | **/packages/* 162 | # except build/, which is used as an MSBuild target. 163 | !**/packages/build/ 164 | # Uncomment if necessary however generally it will be regenerated when needed 165 | #!**/packages/repositories.config 166 | # NuGet v3's project.json files produces more ignoreable files 167 | *.nuget.props 168 | *.nuget.targets 169 | 170 | # Microsoft Azure Build Output 171 | csx/ 172 | *.build.csdef 173 | 174 | # Microsoft Azure Emulator 175 | ecf/ 176 | rcf/ 177 | 178 | # Windows Store app package directories and files 179 | AppPackages/ 180 | BundleArtifacts/ 181 | Package.StoreAssociation.xml 182 | _pkginfo.txt 183 | 184 | # Visual Studio cache files 185 | # files ending in .cache can be ignored 186 | *.[Cc]ache 187 | # but keep track of directories ending in .cache 188 | !*.[Cc]ache/ 189 | 190 | # Others 191 | ClientBin/ 192 | ~$* 193 | *~ 194 | *.dbmdl 195 | *.dbproj.schemaview 196 | *.jfm 197 | *.pfx 198 | *.publishsettings 199 | node_modules/ 200 | orleans.codegen.cs 201 | 202 | # Since there are multiple workflows, uncomment next line to ignore bower_components 203 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 204 | #bower_components/ 205 | 206 | # RIA/Silverlight projects 207 | Generated_Code/ 208 | 209 | # Backup & report files from converting an old project file 210 | # to a newer Visual Studio version. Backup files are not needed, 211 | # because we have git ;-) 212 | _UpgradeReport_Files/ 213 | Backup*/ 214 | UpgradeLog*.XML 215 | UpgradeLog*.htm 216 | 217 | # SQL Server files 218 | *.mdf 219 | *.ldf 220 | 221 | # Business Intelligence projects 222 | *.rdl.data 223 | *.bim.layout 224 | *.bim_*.settings 225 | 226 | # Microsoft Fakes 227 | FakesAssemblies/ 228 | 229 | # GhostDoc plugin setting file 230 | *.GhostDoc.xml 231 | 232 | # Node.js Tools for Visual Studio 233 | .ntvs_analysis.dat 234 | 235 | # Visual Studio 6 build log 236 | *.plg 237 | 238 | # Visual Studio 6 workspace options file 239 | *.opt 240 | 241 | # Visual Studio LightSwitch build output 242 | **/*.HTMLClient/GeneratedArtifacts 243 | **/*.DesktopClient/GeneratedArtifacts 244 | **/*.DesktopClient/ModelManifest.xml 245 | **/*.Server/GeneratedArtifacts 246 | **/*.Server/ModelManifest.xml 247 | _Pvt_Extensions 248 | 249 | # Paket dependency manager 250 | .paket/paket.exe 251 | paket-files/ 252 | 253 | # FAKE - F# Make 254 | .fake/ 255 | 256 | # JetBrains Rider 257 | .idea/ 258 | *.sln.iml 259 | 260 | # CodeRush 261 | .cr/ 262 | 263 | # Python Tools for Visual Studio (PTVS) 264 | __pycache__/ 265 | *.pyc 266 | -------------------------------------------------------------------------------- /custom_components/xiaomi_mijia_ptx/switch.py: -------------------------------------------------------------------------------- 1 | """Support for xiaomi mijia ptx .""" 2 | import asyncio 3 | from functools import partial 4 | import logging 5 | 6 | from homeassistant.components.switch import SwitchEntity 7 | 8 | from miio import ( # pylint: disable=import-error 9 | Device, 10 | DeviceException, 11 | ) 12 | import voluptuous as vol 13 | 14 | from homeassistant.components.switch import PLATFORM_SCHEMA 15 | from homeassistant.const import ( 16 | ATTR_ENTITY_ID, 17 | CONF_HOST, 18 | CONF_NAME, 19 | CONF_TOKEN, 20 | ) 21 | from homeassistant.exceptions import PlatformNotReady 22 | import homeassistant.helpers.config_validation as cv 23 | 24 | from .ptxswitch import PtxSwitch 25 | 26 | _LOGGER = logging.getLogger(__name__) 27 | 28 | DEFAULT_NAME = "Xiaomi Miio PTX Switch" 29 | DATA_KEY = "switch.xiaomi_mijia_ptx" 30 | 31 | CONF_MODEL = "model" 32 | 33 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( 34 | { 35 | vol.Required(CONF_HOST): cv.string, 36 | vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), 37 | vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, 38 | vol.Optional(CONF_MODEL): vol.In( 39 | [ 40 | "090615.switch.switch01", 41 | "090615.switch.switch02", 42 | "090615.switch.switch03", 43 | "090615.switch.xswitch01", 44 | "090615.switch.xswitch02", 45 | "090615.switch.xswitch03", 46 | ] 47 | ), 48 | } 49 | ) 50 | ATTR_MODEL = "model" 51 | 52 | SUCCESS = ["ok"] 53 | 54 | FEATURE_FLAGS_GENERIC = 0 55 | 56 | SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) 57 | 58 | 59 | async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): 60 | """Set up the switch from config.""" 61 | if DATA_KEY not in hass.data: 62 | hass.data[DATA_KEY] = {} 63 | 64 | host = config[CONF_HOST] 65 | token = config[CONF_TOKEN] 66 | name = config[CONF_NAME] 67 | model = config.get(CONF_MODEL) 68 | 69 | _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) 70 | 71 | devices = [] 72 | unique_id = None 73 | 74 | if model is None: 75 | try: 76 | miio_device = Device(host, token) 77 | device_info = await hass.async_add_executor_job(miio_device.info) 78 | model = device_info.model 79 | unique_id = f"{model}-{device_info.mac_address}" 80 | _LOGGER.info( 81 | "%s %s %s detected", 82 | model, 83 | device_info.firmware_version, 84 | device_info.hardware_version, 85 | ) 86 | except DeviceException: 87 | raise PlatformNotReady 88 | 89 | if model in ["090615.switch.switch01","090615.switch.xswitch01"]: 90 | plug = PtxSwitch(host, token, model=model) 91 | device = XiaomiPTXSwitch(name, plug, model, unique_id, 1) 92 | devices.append(device) 93 | hass.data[DATA_KEY][host] = device 94 | elif model in ["090615.switch.switch02","090615.switch.xswitch02"]: 95 | plug = PtxSwitch(host, token, model=model) 96 | device = XiaomiPTXSwitch(name, plug, model, unique_id, 1) 97 | devices.append(device) 98 | hass.data[DATA_KEY][host] = device 99 | 100 | plug2 = PtxSwitch(host, token, model=model) 101 | device2 = XiaomiPTXSwitch(name, plug2, model, unique_id, 2) 102 | devices.append(device2) 103 | hass.data[DATA_KEY][host] = device2 104 | 105 | elif model in ["090615.switch.switch03","090615.switch.xswitch03"]: 106 | plug = PtxSwitch(host, token, model=model) 107 | device = XiaomiPTXSwitch(name, plug, model, unique_id, 1) 108 | devices.append(device) 109 | hass.data[DATA_KEY][host] = device 110 | 111 | plug2 = PtxSwitch(host, token, model=model) 112 | device2 = XiaomiPTXSwitch(name, plug2, model, unique_id, 2) 113 | devices.append(device2) 114 | hass.data[DATA_KEY][host] = device2 115 | 116 | plug3 = PtxSwitch(host, token, model=model) 117 | device3 = XiaomiPTXSwitch(name, plug3, model, unique_id, 3) 118 | devices.append(device3) 119 | hass.data[DATA_KEY][host] = device3 120 | 121 | else: 122 | _LOGGER.error( 123 | "Unsupported device found! Please create an issue at " 124 | "https://github.com/volshebniks/python-miio-ptx/issues " 125 | "and provide the following data: %s", 126 | model, 127 | ) 128 | return False 129 | 130 | async_add_entities(devices, update_before_add=True) 131 | 132 | 133 | class XiaomiPTXSwitch(SwitchEntity): 134 | """Representation of a Xiaomi Plug Generic.""" 135 | 136 | def __init__(self, name, plug, model, unique_id, index): 137 | """Initialize the plug switch.""" 138 | self._name = name 139 | self._plug = plug 140 | self._model = model 141 | self._unique_id = unique_id 142 | self._index = index 143 | 144 | self._icon = "mdi:power-socket" 145 | self._available = False 146 | self._state = None 147 | if model in ["090615.switch.switch01", "090615.switch.switch02", "090615.switch.switch03", 148 | "090615.switch.xswitch01", "090615.switch.xswitch02", "090615.switch.xswitch03"]: 149 | self._state_attrs = {ATTR_MODEL: self._model} 150 | 151 | self._device_features = FEATURE_FLAGS_GENERIC 152 | self._skip_update = False 153 | 154 | @property 155 | def should_poll(self): 156 | """Poll the plug.""" 157 | return True 158 | 159 | @property 160 | def unique_id(self): 161 | """Return an unique ID.""" 162 | return self._unique_id 163 | 164 | @property 165 | def name(self): 166 | """Return the name of the device if any.""" 167 | return f'{self._name}_{str(self._index)}' 168 | 169 | @property 170 | def icon(self): 171 | """Return the icon to use for device if any.""" 172 | return self._icon 173 | 174 | @property 175 | def available(self): 176 | """Return true when state is known.""" 177 | return self._available 178 | 179 | @property 180 | def is_on(self): 181 | """Return true if switch is on.""" 182 | return self._state 183 | 184 | async def _try_command(self, mask_error, func, *args, **kwargs): 185 | """Call a plug command handling error messages.""" 186 | try: 187 | result = await self.hass.async_add_executor_job( 188 | partial(func, *args, **kwargs) 189 | ) 190 | 191 | _LOGGER.debug("Response received from plug: %s", result) 192 | 193 | return result == SUCCESS 194 | except DeviceException as exc: 195 | _LOGGER.error(mask_error, exc) 196 | self._available = False 197 | return False 198 | 199 | async def async_turn_on(self, **kwargs): 200 | """Turn the plug on.""" 201 | result = await self._try_command("Turning the plug on failed.", self._plug.turn_switch, self._index, 1) 202 | 203 | if result: 204 | self._state = True 205 | self._skip_update = True 206 | 207 | async def async_turn_off(self, **kwargs): 208 | """Turn the plug off.""" 209 | result = await self._try_command("Turning the plug off failed.", self._plug.turn_switch, self._index, 0) 210 | 211 | if result: 212 | self._state = False 213 | self._skip_update = True 214 | 215 | async def async_update(self): 216 | """Fetch state from the device.""" 217 | # On state change the device doesn't provide the new state immediately. 218 | if self._skip_update: 219 | self._skip_update = False 220 | return 221 | 222 | try: 223 | state = await self.hass.async_add_executor_job(self._plug.status) 224 | _LOGGER.debug("Got new state: %s", state) 225 | 226 | self._available = True 227 | if self._index == 1: 228 | self._state = state.is_on_1 229 | elif self._index == 2: 230 | self._state = state.is_on_2 231 | elif self._index == 3: 232 | self._state = state.is_on_3 233 | else: 234 | self._state = state.is_on 235 | 236 | except DeviceException as ex: 237 | self._available = False 238 | _LOGGER.error("Got exception while fetching the state: %s", ex) 239 | -------------------------------------------------------------------------------- /custom_components/xiaomi_mijia_ptx/ptxswitch.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from collections import defaultdict 3 | from typing import Dict, Any, Optional 4 | from miio.click_common import command, format_output 5 | from miio.device import Device 6 | 7 | 8 | _LOGGER = logging.getLogger(__name__) 9 | 10 | 11 | MODEL_PTX_SINGLE_WALL_SWITCH = '090615.switch.switch01' 12 | 13 | MODEL_PTX_DUAL_WALL_SWITCH = '090615.switch.switch02' 14 | 15 | MODEL_PTX_TRIPLE_WALL_SWITCH = '090615.switch.switch03' 16 | 17 | MODEL_PTX_HARD_SINGLE_WALL_SWITCH = '090615.switch.xswitch01' 18 | MODEL_PTX_HARD_DUAL_WALL_SWITCH = '090615.switch.xswitch02' 19 | MODEL_PTX_HARD_TRIPLE_WALL_SWITCH = '090615.switch.xswitch03' 20 | 21 | 22 | AVAILABLE_PROPERTIES = { 23 | MODEL_PTX_SINGLE_WALL_SWITCH: [ 24 | "is_on_1", 25 | "switchname1"], 26 | 27 | MODEL_PTX_HARD_SINGLE_WALL_SWITCH: [ 28 | "is_on_1", 29 | "switchname1"], 30 | 31 | MODEL_PTX_DUAL_WALL_SWITCH: [ 32 | "is_on_1", 33 | "is_on_2", 34 | "switchname1", 35 | "switchname2"], 36 | 37 | MODEL_PTX_HARD_DUAL_WALL_SWITCH: [ 38 | "is_on_1", 39 | "is_on_2", 40 | "switchname1", 41 | "switchname2"], 42 | 43 | MODEL_PTX_TRIPLE_WALL_SWITCH: [ 44 | "is_on_1", 45 | "is_on_2", 46 | "is_on_3", 47 | "switchname1", 48 | "switchname2", 49 | "switchname3"], 50 | 51 | MODEL_PTX_HARD_TRIPLE_WALL_SWITCH: [ 52 | "is_on_1", 53 | "is_on_2", 54 | "is_on_3", 55 | "switchname1", 56 | "switchname2", 57 | "switchname3"], 58 | } 59 | 60 | 61 | class PtxSwitchStatus: 62 | # Container for status of PTX switch. 63 | 64 | def __init__(self, data: Dict[str, Any]) -> None: 65 | self.data = data 66 | 67 | def is_on_index(self, index) -> Optional[bool]: 68 | # True if switch {index} is on. 69 | k = "is_on_{}".format(index) 70 | if k in self.data and self.data[k] is not None: 71 | return self.data[k] 72 | return None 73 | 74 | @property 75 | def is_on(self) -> Optional[bool]: 76 | # True if switch 1 is on. 77 | return self.is_on_index(1) 78 | 79 | @property 80 | def is_on_1(self) -> Optional[bool]: 81 | # True if switch 1 is on. 82 | return self.is_on_index(1) 83 | 84 | @property 85 | def is_on_2(self) -> Optional[bool]: 86 | # True if switch 2 is on. 87 | return self.is_on_index(2) 88 | 89 | @property 90 | def is_on_3(self) -> Optional[bool]: 91 | # True if switch 3 is on. 92 | return self.is_on_index(3) 93 | 94 | def switch_name_index(self, index) -> Optional[str]: 95 | # Name of the switch button {index} 96 | k = "switchname{}".format(index) 97 | if k in self.data and self.data[k] is not None: 98 | return self.data[k] 99 | return None 100 | 101 | @property 102 | def switch_name_1(self) -> Optional[str]: 103 | # Name of the switch button 1 104 | return self.switch_name_index(1) 105 | 106 | @property 107 | def switch_name_2(self) -> Optional[str]: 108 | # Name of the switch button 2 109 | return self.switch_name_index(2) 110 | 111 | @property 112 | def switch_name_3(self) -> Optional[str]: 113 | # Name of the switch button 3 114 | return self.switch_name_index(3) 115 | 116 | def __repr__(self) -> str: 117 | s = "" % \ 124 | (self.is_on_1, 125 | self.is_on_2, 126 | self.is_on_3, 127 | self.switch_name_1, 128 | self.switch_name_2, 129 | self.switch_name_3) 130 | return s 131 | 132 | def __json__(self): 133 | return self.data 134 | 135 | 136 | class PtxSwitch(Device): 137 | # Main class representing the PTX switch 138 | 139 | def __init__(self, ip: str = None, token: str = None, start_id: int = 0, 140 | debug: int = 0, lazy_discover: bool = True, 141 | model: str = MODEL_PTX_TRIPLE_WALL_SWITCH) -> None: 142 | super().__init__(ip, token, start_id, debug, lazy_discover) 143 | 144 | if model in AVAILABLE_PROPERTIES: 145 | self.model = model 146 | else: 147 | self.model = MODEL_PTX_TRIPLE_WALL_SWITCH 148 | 149 | @command( 150 | default_output=format_output( 151 | "", 152 | "Switch 1 Status: {result.is_on_1}\n" 153 | "Switch 2 Status: {result.is_on_2}\n" 154 | "Switch 2 Status: {result.is_on_3}\n" 155 | "Switch 1 Name: {result.switch_name_1}\n" 156 | "Switch 2 Name: {result.switch_name_2}\n" 157 | "Switch 3 Name: {result.switch_name_3}\n") 158 | ) 159 | def status(self) -> PtxSwitchStatus: 160 | """ 161 | PTX Triple wall switch payload dump: 162 | 163 | # Query all 3 Switches status 164 | -> {Mi Home App} data= {"id":2468,"method":"get_prop","params":[0,0,0]} 165 | 166 | # Returns On, Off, Off 167 | <- {PTX Switch} data= {"result":[1,0,0,1],"id":2468} 168 | 169 | # Query Switch Name 3 170 | -> {Mi Home App} data= {"id":2471,"method":"get_prop","params":["switchname3"]} 171 | 172 | # Returns Switch Name 3 173 | <- {PTX Switch} data= {"result":["switch3"],"id":2471} 174 | 175 | # Set switch name 1 to 'test name 1' 176 | -> {Mi Home App} data= {"id":2472,"method":"SetSwtichname1","params":["test name 1"]} 177 | 178 | # Didn't firgure out meaning of this return data. 179 | # Switches status were On, Off, Off at this moment 180 | <- {PTX Switch} data= {"result":[0],"id":2472} 181 | 182 | # Turn on switch 1 183 | -> {Mi Home App} data= {"id":2473,"method":"SetSwitch1","params":[1]} 184 | 185 | # Returns status On 186 | <- {PTX Switch} data= {"result":[1],"id":2473} 187 | 188 | # Turn Off switch 1 189 | -> {Mi Home App} data= {"id":2474,"method":"SetSwitch1","params":[0]} 190 | 191 | # Returns status Off 192 | <- {PTX Switch} data= {"result":[0],"id":2474} 193 | 194 | # Turn on all switches 195 | -> {Mi Home App} data= {"id":3381,"method":"SetSwitchAll","params":[1,1,1]} 196 | 197 | # Returns On, On, On 198 | <- {PTX Switch} data= {"result":[1,1,1],"id":3381} 199 | 200 | # Turn off all switches 201 | -> {Mi Home App} data= {"id":3382,"method":"SetSwitchAll","params":[0,0,0]} 202 | 203 | # Returns off, off, off 204 | <- {PTX Switch} data= {"result":[0,0,0],"id":3382} 205 | 206 | """ 207 | # Retrieve properties. 208 | properties = AVAILABLE_PROPERTIES[self.model].copy() 209 | 210 | # Querying switch status require [0,0,0] as the request 211 | # params. Querying other property require property name 212 | # (eg. 'switchname1') as the request param. 213 | 214 | # params for querying status of the switches 215 | params_switch_status = list() 216 | 217 | # params for querying other properties 218 | params_other = list() 219 | 220 | # Query every switch status 221 | 222 | for k in properties: 223 | if k[:5] == "is_on": 224 | params_switch_status.append(0) 225 | else: 226 | params_other.append(k) 227 | 228 | result_1 = self.send( 229 | "get_prop", 230 | params_switch_status 231 | ) 232 | 233 | param_count = len(params_switch_status) 234 | values_count = len(result_1) 235 | if values_count >= param_count: 236 | # return values always have one more than requested. 237 | # get return values only we want 238 | result_1 = result_1[:param_count] 239 | else: 240 | result_1 = [None, None, None] 241 | _LOGGER.debug( 242 | "Count (%s) of requested params does not match the " 243 | "count (%s) of received values.", 244 | param_count, values_count) 245 | 246 | # Query other params 247 | # A single request is limited to 1 properties. Therefore the 248 | # properties are divided into multiple requests 249 | 250 | result_2 = list() 251 | for param in params_other: 252 | value = self.send( 253 | "get_prop", 254 | [param] 255 | ) 256 | if len(value) >= 1: 257 | v = value[0] 258 | if param[:10] == "switchname": 259 | if isinstance(v, str): 260 | result_2.append(v) 261 | else: 262 | _LOGGER.debug("Unexpected type of switchname") 263 | result_2.append(None) 264 | 265 | else: 266 | result_2.append(v) 267 | 268 | else: 269 | result_2.append(None) 270 | _LOGGER.debug("Property %s returns None.", param) 271 | 272 | result = PtxSwitchStatus( 273 | defaultdict(lambda: None, 274 | zip(properties, 275 | result_1 + result_2))) 276 | return result 277 | 278 | def turn_switch(self, index: int, switch_state: int) -> bool: 279 | """ 280 | Turn a switch channel on/off. 281 | index: switch channel index, start from 1. 282 | switch_state: 0 means on, 1 means off. 283 | """ 284 | properties = AVAILABLE_PROPERTIES[self.model] 285 | key = "is_on_{}".format(index) 286 | if key not in properties: 287 | _LOGGER.debug("switch index (%s) not supported.", index) 288 | return False 289 | 290 | result = self.send( 291 | "SetSwitch{}".format(index), 292 | [switch_state] 293 | ) 294 | if result[:1] == [0] or result[:1] == [1]: 295 | return True 296 | else: 297 | _LOGGER.debug("Toogle switch {} failed.".format(index)) 298 | return False 299 | 300 | def turn_on_switch(self, index: int) -> object: 301 | return self.turn_switch(index, 1) 302 | 303 | def turn_off_switch(self, index: int) -> bool: 304 | return self.turn_switch(index, 0) 305 | 306 | def set_switch_name(self, index: int, name: str) -> bool: 307 | """ 308 | Set new name for a switch channel. 309 | index: switch channel index, start from 1. 310 | name: new name. 311 | """ 312 | properties = AVAILABLE_PROPERTIES[self.model] 313 | key = "switchname{}".format(index) 314 | if key not in properties: 315 | _LOGGER.debug("switch index (%s) not supported.", index) 316 | return False 317 | 318 | result = self.send( 319 | "SetSwtichname{}".format(index), 320 | [name] 321 | ) 322 | 323 | if result[:1] == 0 or result[:1] == 1: 324 | return True 325 | else: 326 | _LOGGER.debug( 327 | "Set name of switch {} failed.".format(index) 328 | ) 329 | return False 330 | --------------------------------------------------------------------------------