├── .gitignore ├── LICENSE ├── README.md ├── RELEASE_NOTES.md ├── setup.cfg ├── setup.py └── thinqconnect ├── __init__.py ├── const.py ├── country.py ├── device.py ├── devices ├── __init__.py ├── air_conditioner.py ├── air_purifier.py ├── air_purifier_fan.py ├── ceiling_fan.py ├── connect_device.py ├── const.py ├── cooktop.py ├── dehumidifier.py ├── dish_washer.py ├── dryer.py ├── home_brew.py ├── hood.py ├── humidifier.py ├── kimchi_refrigerator.py ├── microwave_oven.py ├── oven.py ├── plant_cultivator.py ├── refrigerator.py ├── robot_cleaner.py ├── stick_cleaner.py ├── styler.py ├── system_boiler.py ├── ventilator.py ├── washcombo.py ├── washcombo_main.py ├── washcombo_mini.py ├── washer.py ├── washtower.py ├── washtower_dryer.py ├── washtower_washer.py ├── water_heater.py ├── water_purifier.py └── wine_cellar.py ├── integration ├── __init__.py └── homeassistant │ ├── __init__.py │ ├── api.py │ ├── property.py │ ├── specification.py │ ├── state.py │ └── temperature.py ├── mqtt_client.py └── thinq_api.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/git,node,python,pycharm,serverless,virtualenv,visualstudiocode 3 | # Edit at https://www.gitignore.io/?templates=git,node,python,pycharm,serverless,virtualenv,visualstudiocode 4 | 5 | ### Git ### 6 | # Created by git for backups. To disable backups in Git: 7 | # $ git config --global mergetool.keepBackup false 8 | *.orig 9 | .DS_Store 10 | # Created by git when using merge tools for conflicts 11 | *.BACKUP.* 12 | *.BASE.* 13 | *.LOCAL.* 14 | *.REMOTE.* 15 | *_BACKUP_*.txt 16 | *_BASE_*.txt 17 | *_LOCAL_*.txt 18 | *_REMOTE_*.txt 19 | 20 | ### Node ### 21 | # Logs 22 | logs 23 | *.log 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | lerna-debug.log* 28 | 29 | # Diagnostic reports (https://nodejs.org/api/report.html) 30 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 31 | 32 | # Runtime data 33 | pids 34 | *.pid 35 | *.seed 36 | *.pid.lock 37 | 38 | # Directory for instrumented libs generated by jscoverage/JSCover 39 | lib-cov 40 | 41 | # Coverage directory used by tools like istanbul 42 | coverage 43 | *.lcov 44 | 45 | # nyc test coverage 46 | .nyc_output 47 | 48 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 49 | .grunt 50 | 51 | # Bower dependency directory (https://bower.io/) 52 | bower_components 53 | 54 | # node-waf configuration 55 | .lock-wscript 56 | 57 | # Compiled binary addons (https://nodejs.org/api/addons.html) 58 | build/Release 59 | 60 | # Dependency directories 61 | node_modules/ 62 | jspm_packages/ 63 | 64 | # TypeScript v1 declaration files 65 | typings/ 66 | 67 | # TypeScript cache 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | .npm 72 | 73 | # Optional eslint cache 74 | .eslintcache 75 | 76 | # Optional REPL history 77 | .node_repl_history 78 | 79 | # Output of 'npm pack' 80 | *.tgz 81 | 82 | # Yarn Integrity file 83 | .yarn-integrity 84 | 85 | # dotenv environment variables file 86 | .env 87 | .env.test 88 | 89 | # parcel-bundler cache (https://parceljs.org/) 90 | .cache 91 | 92 | # next.js build output 93 | .next 94 | 95 | # nuxt.js build output 96 | .nuxt 97 | 98 | # react / gatsby 99 | public/ 100 | 101 | # vuepress build output 102 | .vuepress/dist 103 | 104 | # Serverless directories 105 | .serverless/ 106 | 107 | # FuseBox cache 108 | .fusebox/ 109 | 110 | # DynamoDB Local files 111 | .dynamodb/ 112 | 113 | ### PyCharm ### 114 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 115 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 116 | 117 | # User-specific stuff 118 | .idea 119 | 120 | # Generated files 121 | .idea/**/contentModel.xml 122 | 123 | # Sensitive or high-churn files 124 | .idea/**/dataSources/ 125 | .idea/**/dataSources.ids 126 | .idea/**/dataSources.local.xml 127 | .idea/**/sqlDataSources.xml 128 | .idea/**/dynamic.xml 129 | .idea/**/uiDesigner.xml 130 | .idea/**/dbnavigator.xml 131 | 132 | # Gradle 133 | .idea/**/gradle.xml 134 | .idea/**/libraries 135 | 136 | # Gradle and Maven with auto-import 137 | # When using Gradle or Maven with auto-import, you should exclude module files, 138 | # since they will be recreated, and may cause churn. Uncomment if using 139 | # auto-import. 140 | # .idea/modules.xml 141 | # .idea/*.iml 142 | # .idea/modules 143 | # *.iml 144 | # *.ipr 145 | 146 | # CMake 147 | cmake-build-*/ 148 | 149 | # Mongo Explorer plugin 150 | .idea/**/mongoSettings.xml 151 | 152 | # File-based project format 153 | *.iws 154 | 155 | # IntelliJ 156 | out/ 157 | 158 | # mpeltonen/sbt-idea plugin 159 | .idea_modules/ 160 | 161 | # JIRA plugin 162 | atlassian-ide-plugin.xml 163 | 164 | # Cursive Clojure plugin 165 | .idea/replstate.xml 166 | 167 | # Crashlytics plugin (for Android Studio and IntelliJ) 168 | com_crashlytics_export_strings.xml 169 | crashlytics.properties 170 | crashlytics-build.properties 171 | fabric.properties 172 | 173 | # Editor-based Rest Client 174 | .idea/httpRequests 175 | 176 | # Android studio 3.1+ serialized cache file 177 | .idea/caches/build_file_checksums.ser 178 | 179 | ### PyCharm Patch ### 180 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 181 | 182 | # *.iml 183 | # modules.xml 184 | # .idea/misc.xml 185 | # *.ipr 186 | 187 | # Sonarlint plugin 188 | .idea/**/sonarlint/ 189 | 190 | # SonarQube Plugin 191 | .idea/**/sonarIssues.xml 192 | 193 | # Markdown Navigator plugin 194 | .idea/**/markdown-navigator.xml 195 | .idea/**/markdown-navigator/ 196 | 197 | ### Python ### 198 | # Byte-compiled / optimized / DLL files 199 | __pycache__/ 200 | *.py[cod] 201 | *$py.class 202 | 203 | # C extensions 204 | *.so 205 | 206 | # Distribution / packaging 207 | .Python 208 | build/ 209 | develop-eggs/ 210 | dist/ 211 | downloads/ 212 | eggs/ 213 | .eggs/ 214 | lib/ 215 | lib64/ 216 | parts/ 217 | sdist/ 218 | var/ 219 | wheels/ 220 | pip-wheel-metadata/ 221 | share/python-wheels/ 222 | *.egg-info/ 223 | .installed.cfg 224 | *.egg 225 | MANIFEST 226 | 227 | # PyInstaller 228 | # Usually these files are written by a python script from a template 229 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 230 | *.manifest 231 | *.spec 232 | 233 | # Installer logs 234 | pip-log.txt 235 | pip-delete-this-directory.txt 236 | 237 | # Unit test / coverage reports 238 | htmlcov/ 239 | .tox/ 240 | .nox/ 241 | .coverage 242 | .coverage.* 243 | nosetests.xml 244 | coverage.xml 245 | *.cover 246 | .hypothesis/ 247 | .pytest_cache/ 248 | test_results.jsonl 249 | 250 | # Translations 251 | *.mo 252 | *.pot 253 | 254 | # Scrapy stuff: 255 | .scrapy 256 | 257 | # Sphinx documentation 258 | docs/_build/ 259 | 260 | # PyBuilder 261 | target/ 262 | 263 | # pyenv 264 | .python-version 265 | 266 | # pipenv 267 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 268 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 269 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 270 | # install all needed dependencies. 271 | #Pipfile.lock 272 | 273 | # celery beat schedule file 274 | celerybeat-schedule 275 | 276 | # SageMath parsed files 277 | *.sage.py 278 | 279 | # Spyder project settings 280 | .spyderproject 281 | .spyproject 282 | 283 | # Rope project settings 284 | .ropeproject 285 | 286 | # Mr Developer 287 | .mr.developer.cfg 288 | .project 289 | .pydevproject 290 | 291 | # mkdocs documentation 292 | /site 293 | 294 | # mypy 295 | .mypy_cache/ 296 | .dmypy.json 297 | dmypy.json 298 | 299 | # Pyre type checker 300 | .pyre/ 301 | 302 | ### Serverless ### 303 | # Ignore build directory 304 | .serverless 305 | 306 | ### VirtualEnv ### 307 | # Virtualenv 308 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 309 | [Bb]in 310 | [Ii]nclude 311 | [Ll]ib 312 | [Ll]ib64 313 | [Ll]ocal 314 | [Ss]cripts 315 | pyvenv.cfg 316 | .venv 317 | env/ 318 | venv/ 319 | ENV/ 320 | env.bak/ 321 | venv.bak/ 322 | pip-selfcheck.json 323 | 324 | ### VisualStudioCode ### 325 | .vscode 326 | 327 | ### VisualStudioCode Patch ### 328 | # Ignore all local history of files 329 | .history 330 | .credentials 331 | tmp/ 332 | docs/ 333 | # End of https://www.gitignore.io/api/git,node,python,pycharm,serverless,virtualenv,visualstudiocode 334 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2024 thinqconnect 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | ## 1.0.6 (2025-04-30) 4 | ### Features 5 | * Add **washer mode** property (washcombo) 6 | * Add **top filter remain percent** property (air_purifier) 7 | * Support **express fridge** property control (refrigerator) 8 | 9 | ## 1.0.5 (2025-03-14) 10 | ### Features 11 | * Add ventilator device 12 | * Support **current job mode** property control (dehumidifier) 13 | 14 | ## 1.0.4 (2025-02-03) 15 | ### Features 16 | * Add **cycle count** property (washer): [#7](https://github.com/thinq-connect/pythinqconnect/issues/7) 17 | * Add **express mode name**, **express fridge** property (refrigerator) 18 | * Add **hop oil** and **flavor info** property for two capsules (home_brew) 19 | * Add **room temp mode**, **room water mode** property (system_boiler) 20 | * Add **two set enabled** property (air_conditioner) 21 | * Add **display light** property (air_conditioner): [#2](https://github.com/thinq-connect/pythinqconnect/issues/2) 22 | * Add **wind direction** property (air_conditioner) 23 | ### Deprecations 24 | * Remove **remain time** properties (hood) 25 | ### Improvements 26 | * Add temperature properties in Fahrenheit (air_conditioner, system_boiler, refrigerator, wine_cellar, water_heater) 27 | * Add **temperature unit** property (oven) 28 | ### Fixes 29 | * Fix **target temperature** property value from dict to number (oven) 30 | ### Documentation 31 | * Update README: Update features roadmap for 2025 32 | 33 | ## 1.0.3 (2024-12-03) 34 | ### Documentation 35 | * Update README: Change the old developer site link to the new one. 36 | 37 | ## 1.0.2 (2024-12-03) 38 | ### Documentation 39 | * Update README: Add notice and update features 40 | 41 | ## 1.0.1 (2024-11-21) 42 | ### Features 43 | * Add **filter remain percent** property (air conditioner, air purifier) 44 | * Remove **end hour** property (plant_cultivator) 45 | 46 | ## 1.0.0 (2024-11-07) 47 | ### Improvements 48 | * Add exception handling with invalid device profile 49 | ### Fixes 50 | * Support device doesn't have operation_mode property (cooktop) 51 | 52 | ## 0.9.9 (2024-10-29) 53 | ### Improvements 54 | * Update notification property in device profile (washcombo) 55 | 56 | ## 0.9.8 (2024-09-25) 57 | ### Initial Release 58 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | 4 | [tool:pytest] 5 | testpaths = tests 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | """ 5 | 6 | import os 7 | 8 | from setuptools import find_packages, setup 9 | 10 | packages = ["thinqconnect"] 11 | 12 | with open(os.path.join("README.md"), "r") as fh: 13 | long_description = fh.read() 14 | 15 | 16 | setup( 17 | name="thinqconnect", 18 | version="1.0.6", 19 | packages=find_packages(exclude=["tests"]), 20 | description="ThinQ Connect Python SDK", 21 | author="ThinQConnect", 22 | author_email="thinq-connect@lge.com", 23 | url="https://github.com/thinq-connect/pythinqconnect", 24 | python_requires=">=3.10", 25 | long_description=long_description, 26 | long_description_content_type="text/markdown", 27 | install_requires=["aiohttp", "awsiotsdk", "pyOpenSSL"], 28 | classifiers=[ 29 | "Programming Language :: Python :: 3", 30 | "License :: OSI Approved :: Apache Software License", 31 | "Operating System :: OS Independent", 32 | "Programming Language :: Python :: 3.10", 33 | "Programming Language :: Python :: 3.11", 34 | ], 35 | ) 36 | -------------------------------------------------------------------------------- /thinqconnect/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | """ 5 | from .const import PROPERTY_READABLE, PROPERTY_WRITABLE, DeviceType 6 | from .country import Country 7 | from .device import BaseDevice 8 | from .devices.air_conditioner import AirConditionerDevice 9 | from .devices.air_purifier import AirPurifierDevice 10 | from .devices.air_purifier_fan import AirPurifierFanDevice 11 | from .devices.ceiling_fan import CeilingFanDevice 12 | from .devices.connect_device import ConnectBaseDevice 13 | from .devices.cooktop import CooktopDevice 14 | from .devices.dehumidifier import DehumidifierDevice 15 | from .devices.dish_washer import DishWasherDevice 16 | from .devices.dryer import DryerDevice 17 | from .devices.home_brew import HomeBrewDevice 18 | from .devices.hood import HoodDevice 19 | from .devices.humidifier import HumidifierDevice 20 | from .devices.kimchi_refrigerator import KimchiRefrigeratorDevice 21 | from .devices.microwave_oven import MicrowaveOvenDevice 22 | from .devices.oven import OvenDevice 23 | from .devices.plant_cultivator import PlantCultivatorDevice 24 | from .devices.refrigerator import RefrigeratorDevice 25 | from .devices.robot_cleaner import RobotCleanerDevice 26 | from .devices.stick_cleaner import StickCleanerDevice 27 | from .devices.styler import StylerDevice 28 | from .devices.system_boiler import SystemBoilerDevice 29 | from .devices.ventilator import VentilatorDevice 30 | from .devices.washcombo_main import WashcomboMainDevice 31 | from .devices.washcombo_mini import WashcomboMiniDevice 32 | from .devices.washer import WasherDevice 33 | from .devices.washtower import WashtowerDevice 34 | from .devices.washtower_dryer import WashtowerDryerDevice 35 | from .devices.washtower_washer import WashtowerWasherDevice 36 | from .devices.water_heater import WaterHeaterDevice 37 | from .devices.water_purifier import WaterPurifierDevice 38 | from .devices.wine_cellar import WineCellarDevice 39 | from .mqtt_client import ThinQMQTTClient 40 | from .thinq_api import ThinQApi, ThinQAPIErrorCodes, ThinQAPIException 41 | 42 | __all__ = [ 43 | "AirConditionerDevice", 44 | "AirPurifierDevice", 45 | "AirPurifierFanDevice", 46 | "CeilingFanDevice", 47 | "CooktopDevice", 48 | "DehumidifierDevice", 49 | "DishWasherDevice", 50 | "DryerDevice", 51 | "HomeBrewDevice", 52 | "HoodDevice", 53 | "HumidifierDevice", 54 | "KimchiRefrigeratorDevice", 55 | "MicrowaveOvenDevice", 56 | "OvenDevice", 57 | "PlantCultivatorDevice", 58 | "RefrigeratorDevice", 59 | "RobotCleanerDevice", 60 | "StickCleanerDevice", 61 | "StylerDevice", 62 | "SystemBoilerDevice", 63 | "WashcomboMainDevice", 64 | "WashcomboMiniDevice", 65 | "WasherDevice", 66 | "WashtowerDevice", 67 | "WashtowerDryerDevice", 68 | "WashtowerWasherDevice", 69 | "WaterHeaterDevice", 70 | "WaterPurifierDevice", 71 | "WineCellarDevice", 72 | "ThinQApi", 73 | "ThinQAPIErrorCodes", 74 | "ThinQAPIException", 75 | "Country", 76 | "BaseDevice", 77 | "DeviceType", 78 | "PROPERTY_READABLE", 79 | "PROPERTY_WRITABLE", 80 | "ConnectBaseDevice", 81 | "ThinQMQTTClient", 82 | "VentilatorDevice", 83 | ] 84 | -------------------------------------------------------------------------------- /thinqconnect/const.py: -------------------------------------------------------------------------------- 1 | """ 2 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | """ 5 | from typing import Final 6 | 7 | API_KEY: Final[str] = "v6GFvkweNo7DK7yD3ylIZ9w52aKBU0eJ7wLXkSR3" 8 | CLIENT_PREFIX: Final[str] = "thinq-open" 9 | 10 | 11 | # Device Type 12 | class DeviceType: 13 | AIR_CONDITIONER = "DEVICE_AIR_CONDITIONER" 14 | SYSTEM_BOILER = "DEVICE_SYSTEM_BOILER" 15 | AIR_PURIFIER = "DEVICE_AIR_PURIFIER" 16 | DISH_WASHER = "DEVICE_DISH_WASHER" 17 | DRYER = "DEVICE_DRYER" 18 | OVEN = "DEVICE_OVEN" 19 | REFRIGERATOR = "DEVICE_REFRIGERATOR" 20 | ROBOT_CLEANER = "DEVICE_ROBOT_CLEANER" 21 | WASHER = "DEVICE_WASHER" 22 | STYLER = "DEVICE_STYLER" 23 | WATER_PURIFIER = "DEVICE_WATER_PURIFIER" 24 | HOOD = "DEVICE_HOOD" 25 | COOKTOP = "DEVICE_COOKTOP" 26 | DEHUMIDIFIER = "DEVICE_DEHUMIDIFIER" 27 | HUMIDIFIER = "DEVICE_HUMIDIFIER" 28 | MICROWAVE_OVEN = "DEVICE_MICROWAVE_OVEN" 29 | WINE_CELLAR = "DEVICE_WINE_CELLAR" 30 | CEILING_FAN = "DEVICE_CEILING_FAN" 31 | WATER_HEATER = "DEVICE_WATER_HEATER" 32 | KIMCHI_REFRIGERATOR = "DEVICE_KIMCHI_REFRIGERATOR" 33 | WASHTOWER_WASHER = "DEVICE_WASHTOWER_WASHER" 34 | WASHTOWER_DRYER = "DEVICE_WASHTOWER_DRYER" 35 | WASHTOWER = "DEVICE_WASHTOWER" 36 | STICK_CLEANER = "DEVICE_STICK_CLEANER" 37 | HOME_BREW = "DEVICE_HOME_BREW" 38 | AIR_PURIFIER_FAN = "DEVICE_AIR_PURIFIER_FAN" 39 | PLANT_CULTIVATOR = "DEVICE_PLANT_CULTIVATOR" 40 | WASHCOMBO_MAIN = "DEVICE_WASHCOMBO_MAIN" 41 | WASHCOMBO_MINI = "DEVICE_WASHCOMBO_MINI" 42 | VENTILATOR = "DEVICE_VENTILATOR" 43 | 44 | 45 | PROPERTY_READABLE: Final[str] = "r" 46 | PROPERTY_WRITABLE: Final[str] = "w" 47 | -------------------------------------------------------------------------------- /thinqconnect/country.py: -------------------------------------------------------------------------------- 1 | """ 2 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | """ 5 | from enum import Enum 6 | 7 | 8 | class Country(str, Enum): 9 | AE = "AE" 10 | AF = "AF" 11 | AG = "AG" 12 | AL = "AL" 13 | AM = "AM" 14 | AO = "AO" 15 | AR = "AR" 16 | AT = "AT" 17 | AU = "AU" 18 | AW = "AW" 19 | AZ = "AZ" 20 | BA = "BA" 21 | BB = "BB" 22 | BD = "BD" 23 | BE = "BE" 24 | BF = "BF" 25 | BG = "BG" 26 | BH = "BH" 27 | BJ = "BJ" 28 | BO = "BO" 29 | BR = "BR" 30 | BS = "BS" 31 | BY = "BY" 32 | BZ = "BZ" 33 | CA = "CA" 34 | CD = "CD" 35 | CF = "CF" 36 | CG = "CG" 37 | CH = "CH" 38 | CI = "CI" 39 | CL = "CL" 40 | CM = "CM" 41 | CN = "CN" 42 | CO = "CO" 43 | CR = "CR" 44 | CU = "CU" 45 | CV = "CV" 46 | CY = "CY" 47 | CZ = "CZ" 48 | DE = "DE" 49 | DJ = "DJ" 50 | DK = "DK" 51 | DM = "DM" 52 | DO = "DO" 53 | DZ = "DZ" 54 | EC = "EC" 55 | EE = "EE" 56 | EG = "EG" 57 | ES = "ES" 58 | ET = "ET" 59 | FI = "FI" 60 | FR = "FR" 61 | GA = "GA" 62 | GB = "GB" 63 | GD = "GD" 64 | GE = "GE" 65 | GH = "GH" 66 | GM = "GM" 67 | GN = "GN" 68 | GQ = "GQ" 69 | GR = "GR" 70 | GT = "GT" 71 | GY = "GY" 72 | HK = "HK" 73 | HN = "HN" 74 | HR = "HR" 75 | HT = "HT" 76 | HU = "HU" 77 | ID = "ID" 78 | IE = "IE" 79 | IL = "IL" 80 | IN = "IN" 81 | IQ = "IQ" 82 | IR = "IR" 83 | IS = "IS" 84 | IT = "IT" 85 | JM = "JM" 86 | JO = "JO" 87 | JP = "JP" 88 | KE = "KE" 89 | KG = "KG" 90 | KH = "KH" 91 | KN = "KN" 92 | KR = "KR" 93 | KW = "KW" 94 | KZ = "KZ" 95 | LA = "LA" 96 | LB = "LB" 97 | LC = "LC" 98 | LK = "LK" 99 | LR = "LR" 100 | LT = "LT" 101 | LU = "LU" 102 | LV = "LV" 103 | LY = "LY" 104 | MA = "MA" 105 | MD = "MD" 106 | ME = "ME" 107 | MK = "MK" 108 | ML = "ML" 109 | MM = "MM" 110 | MR = "MR" 111 | MT = "MT" 112 | MU = "MU" 113 | MW = "MW" 114 | MX = "MX" 115 | MY = "MY" 116 | NE = "NE" 117 | NG = "NG" 118 | NI = "NI" 119 | NL = "NL" 120 | NO = "NO" 121 | NP = "NP" 122 | NZ = "NZ" 123 | OM = "OM" 124 | PA = "PA" 125 | PE = "PE" 126 | PH = "PH" 127 | PK = "PK" 128 | PL = "PL" 129 | PR = "PR" 130 | PS = "PS" 131 | PT = "PT" 132 | PY = "PY" 133 | QA = "QA" 134 | RO = "RO" 135 | RS = "RS" 136 | RU = "RU" 137 | RW = "RW" 138 | SA = "SA" 139 | SD = "SD" 140 | SE = "SE" 141 | SG = "SG" 142 | SI = "SI" 143 | SK = "SK" 144 | SL = "SL" 145 | SN = "SN" 146 | SO = "SO" 147 | SR = "SR" 148 | ST = "ST" 149 | SV = "SV" 150 | SY = "SY" 151 | TD = "TD" 152 | TG = "TG" 153 | TH = "TH" 154 | TN = "TN" 155 | TR = "TR" 156 | TT = "TT" 157 | TW = "TW" 158 | TZ = "TZ" 159 | UA = "UA" 160 | UG = "UG" 161 | US = "US" 162 | UY = "UY" 163 | UZ = "UZ" 164 | VC = "VC" 165 | VE = "VE" 166 | VN = "VN" 167 | XK = "XK" 168 | YE = "YE" 169 | ZA = "ZA" 170 | ZM = "ZM" 171 | 172 | def __str__(self): 173 | return self.name 174 | 175 | 176 | class DomainPrefix(str, Enum): 177 | KIC = "kic" 178 | AIC = "aic" 179 | EIC = "eic" 180 | 181 | def __str__(self): 182 | return self.name 183 | 184 | 185 | SUPPORTED_COUNTRIES = { 186 | DomainPrefix.KIC: [ 187 | Country.AU, 188 | Country.BD, 189 | Country.CN, 190 | Country.HK, 191 | Country.ID, 192 | Country.IN, 193 | Country.JP, 194 | Country.KH, 195 | Country.KR, 196 | Country.LA, 197 | Country.LK, 198 | Country.MM, 199 | Country.MY, 200 | Country.NP, 201 | Country.NZ, 202 | Country.PH, 203 | Country.SG, 204 | Country.TH, 205 | Country.TW, 206 | Country.VN, 207 | ], 208 | DomainPrefix.AIC: [ 209 | Country.AG, 210 | Country.AR, 211 | Country.AW, 212 | Country.BB, 213 | Country.BO, 214 | Country.BR, 215 | Country.BS, 216 | Country.BZ, 217 | Country.CA, 218 | Country.CL, 219 | Country.CO, 220 | Country.CR, 221 | Country.CU, 222 | Country.DM, 223 | Country.DO, 224 | Country.EC, 225 | Country.GD, 226 | Country.GT, 227 | Country.GY, 228 | Country.HN, 229 | Country.HT, 230 | Country.JM, 231 | Country.KN, 232 | Country.LC, 233 | Country.MX, 234 | Country.NI, 235 | Country.PA, 236 | Country.PE, 237 | Country.PR, 238 | Country.PY, 239 | Country.SR, 240 | Country.SV, 241 | Country.TT, 242 | Country.US, 243 | Country.UY, 244 | Country.VC, 245 | Country.VE, 246 | ], 247 | DomainPrefix.EIC: [ 248 | Country.AE, 249 | Country.AF, 250 | Country.AL, 251 | Country.AM, 252 | Country.AO, 253 | Country.AT, 254 | Country.AZ, 255 | Country.BA, 256 | Country.BE, 257 | Country.BF, 258 | Country.BG, 259 | Country.BH, 260 | Country.BJ, 261 | Country.BY, 262 | Country.CD, 263 | Country.CF, 264 | Country.CG, 265 | Country.CH, 266 | Country.CI, 267 | Country.CM, 268 | Country.CV, 269 | Country.CY, 270 | Country.CZ, 271 | Country.DE, 272 | Country.DJ, 273 | Country.DK, 274 | Country.DZ, 275 | Country.EE, 276 | Country.EG, 277 | Country.ES, 278 | Country.ET, 279 | Country.FI, 280 | Country.FR, 281 | Country.GA, 282 | Country.GB, 283 | Country.GE, 284 | Country.GH, 285 | Country.GM, 286 | Country.GN, 287 | Country.GQ, 288 | Country.GR, 289 | Country.HR, 290 | Country.HU, 291 | Country.IE, 292 | Country.IL, 293 | Country.IQ, 294 | Country.IR, 295 | Country.IS, 296 | Country.IT, 297 | Country.JO, 298 | Country.KE, 299 | Country.KG, 300 | Country.KW, 301 | Country.KZ, 302 | Country.LB, 303 | Country.LR, 304 | Country.LT, 305 | Country.LU, 306 | Country.LV, 307 | Country.LY, 308 | Country.MA, 309 | Country.MD, 310 | Country.ME, 311 | Country.MK, 312 | Country.ML, 313 | Country.MR, 314 | Country.MT, 315 | Country.MU, 316 | Country.MW, 317 | Country.NE, 318 | Country.NG, 319 | Country.NL, 320 | Country.NO, 321 | Country.OM, 322 | Country.PK, 323 | Country.PL, 324 | Country.PS, 325 | Country.PT, 326 | Country.QA, 327 | Country.RO, 328 | Country.RS, 329 | Country.RU, 330 | Country.RW, 331 | Country.SA, 332 | Country.SD, 333 | Country.SE, 334 | Country.SI, 335 | Country.SK, 336 | Country.SL, 337 | Country.SN, 338 | Country.SO, 339 | Country.ST, 340 | Country.SY, 341 | Country.TD, 342 | Country.TG, 343 | Country.TN, 344 | Country.TR, 345 | Country.TZ, 346 | Country.UA, 347 | Country.UG, 348 | Country.UZ, 349 | Country.XK, 350 | Country.YE, 351 | Country.ZA, 352 | Country.ZM, 353 | ], 354 | } 355 | 356 | 357 | def get_region_from_country(country_code: Country) -> DomainPrefix: 358 | for domain_prefix in DomainPrefix: 359 | if country_code in SUPPORTED_COUNTRIES[domain_prefix]: 360 | return domain_prefix 361 | 362 | raise RuntimeError("Not supported country_code.") 363 | -------------------------------------------------------------------------------- /thinqconnect/device.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """ 4 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 5 | * SPDX-License-Identifier: Apache-2.0 6 | """ 7 | """class for base device""" 8 | 9 | from .thinq_api import ThinQApi 10 | 11 | 12 | class BaseDevice: 13 | """The base implementation of LG ThinQ Device.""" 14 | 15 | thinq_api: ThinQApi 16 | 17 | def __init__( 18 | self, 19 | thinq_api: ThinQApi, 20 | device_id: str, 21 | device_type: str, 22 | model_name: str, 23 | alias: str, 24 | reportable: bool, 25 | ): 26 | self.thinq_api = thinq_api 27 | self.device_id = device_id 28 | self.device_type = device_type 29 | self.model_name = model_name 30 | self.alias = alias 31 | self.reportable = reportable 32 | 33 | @property 34 | def device_type(self) -> str: 35 | return self._device_type 36 | 37 | @device_type.setter 38 | def device_type(self, device_type: str): 39 | self._device_type = device_type 40 | 41 | @property 42 | def model_name(self) -> str: 43 | return self._model_name 44 | 45 | @model_name.setter 46 | def model_name(self, model_name: str): 47 | self._model_name = model_name 48 | 49 | @property 50 | def alias(self) -> str: 51 | return self._alias 52 | 53 | @alias.setter 54 | def alias(self, alias: str): 55 | self._alias = alias 56 | 57 | @property 58 | def reportable(self) -> bool: 59 | return self._reportable 60 | 61 | @reportable.setter 62 | def reportable(self, reportable: bool): 63 | self._reportable = reportable 64 | 65 | @property 66 | def device_id(self) -> str: 67 | return self._device_id 68 | 69 | @device_id.setter 70 | def device_id(self, device_id: str): 71 | self._device_id = device_id 72 | -------------------------------------------------------------------------------- /thinqconnect/devices/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | """ 5 | -------------------------------------------------------------------------------- /thinqconnect/devices/air_purifier.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """ 4 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 5 | * SPDX-License-Identifier: Apache-2.0 6 | """ 7 | from typing import Any 8 | 9 | from ..thinq_api import ThinQApi 10 | from .connect_device import ConnectBaseDevice, ConnectDeviceProfile 11 | from .const import Property, Resource 12 | 13 | 14 | class AirPurifierProfile(ConnectDeviceProfile): 15 | def __init__(self, profile: dict[str, Any]): 16 | super().__init__( 17 | profile=profile, 18 | resource_map={ 19 | "airPurifierJobMode": Resource.AIR_PURIFIER_JOB_MODE, 20 | "operation": Resource.OPERATION, 21 | "timer": Resource.TIMER, 22 | "airFlow": Resource.AIR_FLOW, 23 | "airQualitySensor": Resource.AIR_QUALITY_SENSOR, 24 | "filterInfo": Resource.FILTER_INFO, 25 | }, 26 | profile_map={ 27 | "airPurifierJobMode": { 28 | "currentJobMode": Property.CURRENT_JOB_MODE, 29 | "personalizationMode": Property.PERSONALIZATION_MODE, 30 | }, 31 | "operation": {"airPurifierOperationMode": Property.AIR_PURIFIER_OPERATION_MODE}, 32 | "timer": { 33 | "absoluteHourToStart": Property.ABSOLUTE_HOUR_TO_START, 34 | "absoluteMinuteToStart": Property.ABSOLUTE_MINUTE_TO_START, 35 | "absoluteHourToStop": Property.ABSOLUTE_HOUR_TO_STOP, 36 | "absoluteMinuteToStop": Property.ABSOLUTE_MINUTE_TO_STOP, 37 | }, 38 | "airFlow": { 39 | "windStrength": Property.WIND_STRENGTH, 40 | }, 41 | "airQualitySensor": { 42 | "monitoringEnabled": Property.MONITORING_ENABLED, 43 | "PM1": Property.PM1, 44 | "PM2": Property.PM2, 45 | "PM10": Property.PM10, 46 | "odor": Property.ODOR, 47 | "odorLevel": Property.ODOR_LEVEL, 48 | "humidity": Property.HUMIDITY, 49 | "totalPollution": Property.TOTAL_POLLUTION, 50 | "totalPollutionLevel": Property.TOTAL_POLLUTION_LEVEL, 51 | }, 52 | "filterInfo": { 53 | "topFilterRemainPercent": Property.TOP_FILTER_REMAIN_PERCENT, 54 | "filterRemainPercent": Property.FILTER_REMAIN_PERCENT, 55 | }, 56 | }, 57 | ) 58 | 59 | 60 | class AirPurifierDevice(ConnectBaseDevice): 61 | _CUSTOM_SET_PROPERTY_NAME = { 62 | Property.ABSOLUTE_HOUR_TO_START: "absolute_time_to_start", 63 | Property.ABSOLUTE_MINUTE_TO_START: "absolute_time_to_start", 64 | Property.ABSOLUTE_HOUR_TO_STOP: "absolute_time_to_stop", 65 | Property.ABSOLUTE_MINUTE_TO_STOP: "absolute_time_to_stop", 66 | } 67 | 68 | def __init__( 69 | self, 70 | thinq_api: ThinQApi, 71 | device_id: str, 72 | device_type: str, 73 | model_name: str, 74 | alias: str, 75 | reportable: bool, 76 | profile: dict[str, Any], 77 | ): 78 | super().__init__( 79 | thinq_api=thinq_api, 80 | device_id=device_id, 81 | device_type=device_type, 82 | model_name=model_name, 83 | alias=alias, 84 | reportable=reportable, 85 | profiles=AirPurifierProfile(profile=profile), 86 | ) 87 | 88 | @property 89 | def profiles(self) -> AirPurifierProfile: 90 | return self._profiles 91 | 92 | async def set_current_job_mode(self, job_mode: str) -> dict | None: 93 | return await self.do_enum_attribute_command(Property.CURRENT_JOB_MODE, job_mode) 94 | 95 | async def set_air_purifier_operation_mode(self, operation_mode: str) -> dict | None: 96 | return await self.do_enum_attribute_command(Property.AIR_PURIFIER_OPERATION_MODE, operation_mode) 97 | 98 | async def set_absolute_time_to_start(self, hour: int, minute: int) -> dict | None: 99 | return await self.do_multi_attribute_command( 100 | { 101 | Property.ABSOLUTE_HOUR_TO_START: hour, 102 | Property.ABSOLUTE_MINUTE_TO_START: minute, 103 | } 104 | ) 105 | 106 | async def set_absolute_time_to_stop(self, hour: int, minute: int) -> dict | None: 107 | return await self.do_multi_attribute_command( 108 | { 109 | Property.ABSOLUTE_HOUR_TO_STOP: hour, 110 | Property.ABSOLUTE_MINUTE_TO_STOP: minute, 111 | } 112 | ) 113 | 114 | async def set_wind_strength(self, wind_strength: str) -> dict | None: 115 | return await self.do_enum_attribute_command(Property.WIND_STRENGTH, wind_strength) 116 | -------------------------------------------------------------------------------- /thinqconnect/devices/air_purifier_fan.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """ 4 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 5 | * SPDX-License-Identifier: Apache-2.0 6 | """ 7 | from typing import Any 8 | 9 | from ..thinq_api import ThinQApi 10 | from .connect_device import ConnectBaseDevice, ConnectDeviceProfile 11 | from .const import Property, Resource 12 | 13 | 14 | class AirPurifierFanProfile(ConnectDeviceProfile): 15 | def __init__(self, profile: dict[str, Any]): 16 | super().__init__( 17 | profile=profile, 18 | resource_map={ 19 | "airFanJobMode": Resource.AIR_FAN_JOB_MODE, 20 | "operation": Resource.OPERATION, 21 | "timer": Resource.TIMER, 22 | "sleepTimer": Resource.SLEEP_TIMER, 23 | "airFlow": Resource.AIR_FLOW, 24 | "airQualitySensor": Resource.AIR_QUALITY_SENSOR, 25 | "display": Resource.DISPLAY, 26 | "misc": Resource.MISC, 27 | }, 28 | profile_map={ 29 | "airFanJobMode": {"currentJobMode": Property.CURRENT_JOB_MODE}, 30 | "operation": {"airFanOperationMode": Property.AIR_FAN_OPERATION_MODE}, 31 | "timer": { 32 | "absoluteHourToStart": Property.ABSOLUTE_HOUR_TO_START, 33 | "absoluteMinuteToStart": Property.ABSOLUTE_MINUTE_TO_START, 34 | "absoluteHourToStop": Property.ABSOLUTE_HOUR_TO_STOP, 35 | "absoluteMinuteToStop": Property.ABSOLUTE_MINUTE_TO_STOP, 36 | }, 37 | "sleepTimer": { 38 | "relativeHourToStop": Property.SLEEP_TIMER_RELATIVE_HOUR_TO_STOP, 39 | "relativeMinuteToStop": Property.SLEEP_TIMER_RELATIVE_MINUTE_TO_STOP, 40 | }, 41 | "airFlow": { 42 | "warmMode": Property.WARM_MODE, 43 | "windTemperature": Property.WIND_TEMPERATURE, 44 | "windStrength": Property.WIND_STRENGTH, 45 | "windAngle": Property.WIND_ANGLE, 46 | }, 47 | "airQualitySensor": { 48 | "monitoringEnabled": Property.MONITORING_ENABLED, 49 | "PM1": Property.PM1, 50 | "PM2": Property.PM2, 51 | "PM10": Property.PM10, 52 | "humidity": Property.HUMIDITY, 53 | "temperature": Property.TEMPERATURE, 54 | "odor": Property.ODOR, 55 | "odorLevel": Property.ODOR_LEVEL, 56 | "totalPollution": Property.TOTAL_POLLUTION, 57 | "totalPollutionLevel": Property.TOTAL_POLLUTION_LEVEL, 58 | }, 59 | "display": {"light": Property.DISPLAY_LIGHT}, 60 | "misc": {"uvNano": Property.UV_NANO}, 61 | }, 62 | ) 63 | 64 | 65 | class AirPurifierFanDevice(ConnectBaseDevice): 66 | _CUSTOM_SET_PROPERTY_NAME = { 67 | Property.ABSOLUTE_HOUR_TO_START: "absolute_time_to_start", 68 | Property.ABSOLUTE_MINUTE_TO_START: "absolute_time_to_start", 69 | Property.ABSOLUTE_HOUR_TO_STOP: "absolute_time_to_stop", 70 | Property.ABSOLUTE_MINUTE_TO_STOP: "absolute_time_to_stop", 71 | Property.SLEEP_TIMER_RELATIVE_HOUR_TO_STOP: "sleep_timer_relative_time_to_stop", 72 | Property.SLEEP_TIMER_RELATIVE_MINUTE_TO_STOP: "sleep_timer_relative_time_to_stop", 73 | } 74 | 75 | def __init__( 76 | self, 77 | thinq_api: ThinQApi, 78 | device_id: str, 79 | device_type: str, 80 | model_name: str, 81 | alias: str, 82 | reportable: bool, 83 | profile: dict[str, Any], 84 | ): 85 | super().__init__( 86 | thinq_api=thinq_api, 87 | device_id=device_id, 88 | device_type=device_type, 89 | model_name=model_name, 90 | alias=alias, 91 | reportable=reportable, 92 | profiles=AirPurifierFanProfile(profile=profile), 93 | ) 94 | 95 | @property 96 | def profiles(self) -> AirPurifierFanProfile: 97 | return self._profiles 98 | 99 | async def set_current_job_mode(self, job_mode: str) -> dict | None: 100 | return await self.do_enum_attribute_command(Property.CURRENT_JOB_MODE, job_mode) 101 | 102 | async def set_air_fan_operation_mode(self, operation_mode: str) -> dict | None: 103 | return await self.do_enum_attribute_command(Property.AIR_FAN_OPERATION_MODE, operation_mode) 104 | 105 | async def set_absolute_time_to_start(self, hour: int, minute: int) -> dict | None: 106 | return await self.do_multi_attribute_command( 107 | { 108 | Property.ABSOLUTE_HOUR_TO_START: hour, 109 | Property.ABSOLUTE_MINUTE_TO_START: minute, 110 | } 111 | ) 112 | 113 | async def set_absolute_time_to_stop(self, hour: int, minute: int) -> dict | None: 114 | return await self.do_multi_attribute_command( 115 | { 116 | Property.ABSOLUTE_HOUR_TO_STOP: hour, 117 | Property.ABSOLUTE_MINUTE_TO_STOP: minute, 118 | } 119 | ) 120 | 121 | async def set_sleep_timer_relative_time_to_stop(self, hour: int, minute: int = 0) -> dict | None: 122 | return await self.do_multi_attribute_command( 123 | { 124 | Property.SLEEP_TIMER_RELATIVE_HOUR_TO_STOP: hour, 125 | **({Property.SLEEP_TIMER_RELATIVE_MINUTE_TO_STOP: minute} if minute != 0 else {}), 126 | } 127 | ) 128 | 129 | async def set_warm_mode(self, warm_mode: str) -> dict | None: 130 | return await self.do_enum_attribute_command(Property.WARM_MODE, warm_mode) 131 | 132 | async def set_wind_temperature(self, wind_temperature: int | float) -> dict | None: 133 | return await self.do_attribute_command(Property.WIND_TEMPERATURE, wind_temperature) 134 | 135 | async def set_wind_strength(self, wind_strength: str) -> dict | None: 136 | return await self.do_enum_attribute_command(Property.WIND_STRENGTH, wind_strength) 137 | 138 | async def set_wind_angle(self, wind_angle: str) -> dict | None: 139 | return await self.do_enum_attribute_command(Property.WIND_ANGLE, wind_angle) 140 | 141 | async def set_display_light(self, display_light: str) -> dict | None: 142 | return await self.do_enum_attribute_command(Property.DISPLAY_LIGHT, display_light) 143 | 144 | async def set_uv_nano(self, uv_nano: str) -> dict | None: 145 | return await self.do_enum_attribute_command(Property.UV_NANO, uv_nano) 146 | -------------------------------------------------------------------------------- /thinqconnect/devices/ceiling_fan.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """ 4 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 5 | * SPDX-License-Identifier: Apache-2.0 6 | """ 7 | from typing import Any 8 | 9 | from ..thinq_api import ThinQApi 10 | from .connect_device import ConnectBaseDevice, ConnectDeviceProfile 11 | from .const import Property, Resource 12 | 13 | 14 | class CeilingFanProfile(ConnectDeviceProfile): 15 | def __init__(self, profile: dict[str, Any]): 16 | super().__init__( 17 | profile=profile, 18 | resource_map={"airFlow": Resource.AIR_FLOW, "operation": Resource.OPERATION}, 19 | profile_map={ 20 | "airFlow": {"windStrength": Property.WIND_STRENGTH}, 21 | "operation": {"ceilingfanOperationMode": Property.CEILING_FAN_OPERATION_MODE}, 22 | }, 23 | ) 24 | 25 | 26 | class CeilingFanDevice(ConnectBaseDevice): 27 | """CeilingFan Property.""" 28 | 29 | def __init__( 30 | self, 31 | thinq_api: ThinQApi, 32 | device_id: str, 33 | device_type: str, 34 | model_name: str, 35 | alias: str, 36 | reportable: bool, 37 | profile: dict[str, Any], 38 | ): 39 | super().__init__( 40 | thinq_api=thinq_api, 41 | device_id=device_id, 42 | device_type=device_type, 43 | model_name=model_name, 44 | alias=alias, 45 | reportable=reportable, 46 | profiles=CeilingFanProfile(profile=profile), 47 | ) 48 | 49 | @property 50 | def profiles(self) -> CeilingFanProfile: 51 | return self._profiles 52 | 53 | async def set_wind_strength(self, wind_strength: str) -> dict | None: 54 | return await self.do_enum_attribute_command(Property.WIND_STRENGTH, wind_strength) 55 | 56 | async def set_ceiling_fan_operation_mode(self, mode: str) -> dict | None: 57 | return await self.do_enum_attribute_command(Property.CEILING_FAN_OPERATION_MODE, mode) 58 | -------------------------------------------------------------------------------- /thinqconnect/devices/const.py: -------------------------------------------------------------------------------- 1 | """ 2 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | """ 5 | from enum import StrEnum, auto 6 | 7 | 8 | class Resource(StrEnum): 9 | AIR_CON_JOB_MODE = auto() 10 | AIR_FAN_JOB_MODE = auto() 11 | AIR_FLOW = auto() 12 | AIR_PURIFIER_JOB_MODE = auto() 13 | AIR_QUALITY_SENSOR = auto() 14 | BATTERY = auto() 15 | BOILER_JOB_MODE = auto() 16 | COOK = auto() 17 | COOKING_ZONE = auto() 18 | CYCLE = auto() 19 | DEHUMIDIFIER_JOB_MODE = auto() 20 | DETERGENT = auto() 21 | DISH_WASHING_COURSE = auto() 22 | DISH_WASHING_STATUS = auto() 23 | DISPLAY = auto() 24 | DOOR_STATUS = auto() 25 | ECO_FRIENDLY = auto() 26 | FILTER_INFO = auto() 27 | HUMIDIFIER_JOB_MODE = auto() 28 | HUMIDITY = auto() 29 | INFO = auto() 30 | LAMP = auto() 31 | LIGHT = auto() 32 | MISC = auto() 33 | MOOD_LAMP = auto() 34 | OPERATION = auto() 35 | MODE = auto() 36 | POWER = auto() 37 | POWER_SAVE = auto() 38 | PREFERENCE = auto() 39 | RECIPE = auto() 40 | REFRIGERATION = auto() 41 | REMOTE_CONTROL_ENABLE = auto() 42 | ROBOT_CLEANER_JOB_MODE = auto() 43 | RUN_STATE = auto() 44 | SABBATH = auto() 45 | SLEEP_TIMER = auto() 46 | STICK_CLEANER_JOB_MODE = auto() 47 | TEMPERATURE = auto() 48 | TIMER = auto() 49 | TWO_SET_TEMPERATURE = auto() 50 | HOT_WATER_TEMPERATURE = auto() 51 | ROOM_TEMPERATURE = auto() 52 | VENTILATION = auto() 53 | WATER_FILTER_INFO = auto() 54 | WATER_HEATER_JOB_MODE = auto() 55 | WATER_INFO = auto() 56 | WIND_DIRECTION = auto() 57 | VENTILATOR_JOB_MODE = auto() 58 | 59 | 60 | class Property(StrEnum): 61 | ABSOLUTE_HOUR_TO_START = auto() 62 | ABSOLUTE_HOUR_TO_STOP = auto() 63 | ABSOLUTE_MINUTE_TO_START = auto() 64 | ABSOLUTE_MINUTE_TO_STOP = auto() 65 | AIR_CLEAN_OPERATION_MODE = auto() 66 | AIR_CON_OPERATION_MODE = auto() 67 | AIR_FAN_OPERATION_MODE = auto() 68 | AIR_PURIFIER_OPERATION_MODE = auto() 69 | AUTO_MODE = auto() 70 | BATTERY_LEVEL = auto() 71 | BATTERY_PERCENT = auto() 72 | BEER_REMAIN = auto() 73 | BOILER_OPERATION_MODE = auto() 74 | BRIGHTNESS = auto() 75 | CEILING_FAN_OPERATION_MODE = auto() 76 | CLEAN_LIGHT_REMINDER = auto() 77 | CLEAN_OPERATION_MODE = auto() 78 | CO2 = auto() 79 | COCK_STATE = auto() 80 | COOK_MODE = auto() 81 | COOL_MAX_TEMPERATURE = auto() 82 | COOL_MAX_TEMPERATURE_C = auto() 83 | COOL_MAX_TEMPERATURE_F = auto() 84 | COOL_MIN_TEMPERATURE = auto() 85 | COOL_MIN_TEMPERATURE_C = auto() 86 | COOL_MIN_TEMPERATURE_F = auto() 87 | COOL_TARGET_TEMPERATURE = auto() 88 | COOL_TARGET_TEMPERATURE_C = auto() 89 | COOL_TARGET_TEMPERATURE_F = auto() 90 | CURRENT_DISH_WASHING_COURSE = auto() 91 | CURRENT_HUMIDITY = auto() 92 | CURRENT_JOB_MODE = auto() 93 | CURRENT_STATE = auto() 94 | CURRENT_TEMPERATURE = auto() 95 | CURRENT_TEMPERATURE_C = auto() 96 | CURRENT_TEMPERATURE_F = auto() 97 | CYCLE_COUNT = auto() 98 | DAY_TARGET_TEMPERATURE = auto() 99 | DEHUMIDIFIER_OPERATION_MODE = auto() 100 | DETERGENT_SETTING = auto() 101 | DISH_WASHER_OPERATION_MODE = auto() 102 | DISPLAY_LIGHT = auto() 103 | DOOR_STATE = auto() 104 | DRYER_OPERATION_MODE = auto() 105 | DURATION = auto() 106 | ECO_FRIENDLY_MODE = auto() 107 | ELAPSED_DAY_STATE = auto() 108 | ELAPSED_DAY_TOTAL = auto() 109 | END_HOUR = auto() 110 | END_MINUTE = auto() 111 | EXPRESS_FRIDGE = auto() 112 | EXPRESS_MODE = auto() 113 | EXPRESS_MODE_NAME = auto() 114 | FAN_SPEED = auto() 115 | FILTER_LIFETIME = auto() 116 | TOP_FILTER_REMAIN_PERCENT = auto() 117 | FILTER_REMAIN_PERCENT = auto() 118 | FLAVOR_CAPSULE_1 = auto() 119 | FLAVOR_CAPSULE_2 = auto() 120 | FLAVOR_INFO = auto() 121 | FRESH_AIR_FILTER = auto() 122 | GROWTH_MODE = auto() 123 | HEAT_MAX_TEMPERATURE = auto() 124 | HEAT_MAX_TEMPERATURE_C = auto() 125 | HEAT_MAX_TEMPERATURE_F = auto() 126 | HEAT_MIN_TEMPERATURE = auto() 127 | HEAT_MIN_TEMPERATURE_C = auto() 128 | HEAT_MIN_TEMPERATURE_F = auto() 129 | HEAT_TARGET_TEMPERATURE = auto() 130 | HEAT_TARGET_TEMPERATURE_C = auto() 131 | HEAT_TARGET_TEMPERATURE_F = auto() 132 | HOOD_OPERATION_MODE = auto() 133 | HOP_OIL_CAPSULE_1 = auto() 134 | HOP_OIL_CAPSULE_2 = auto() 135 | HOP_OIL_INFO = auto() 136 | HOT_WATER_MODE = auto() 137 | HOT_WATER_CURRENT_TEMPERATURE_C = auto() 138 | HOT_WATER_CURRENT_TEMPERATURE_F = auto() 139 | HOT_WATER_MAX_TEMPERATURE_C = auto() 140 | HOT_WATER_MAX_TEMPERATURE_F = auto() 141 | HOT_WATER_MIN_TEMPERATURE_C = auto() 142 | HOT_WATER_MIN_TEMPERATURE_F = auto() 143 | HOT_WATER_TARGET_TEMPERATURE_C = auto() 144 | HOT_WATER_TARGET_TEMPERATURE_F = auto() 145 | HOT_WATER_TEMPERATURE_UNIT = auto() 146 | HUMIDIFIER_OPERATION_MODE = auto() 147 | HUMIDITY = auto() 148 | HYGIENE_DRY_MODE = auto() 149 | LAMP_BRIGHTNESS = auto() 150 | LIGHT_BRIGHTNESS = auto() 151 | LIGHT_STATUS = auto() 152 | MACHINE_CLEAN_REMINDER = auto() 153 | MONITORING_ENABLED = auto() 154 | MOOD_LAMP_STATE = auto() 155 | NIGHT_TARGET_TEMPERATURE = auto() 156 | ODOR = auto() 157 | ODOR_LEVEL = auto() 158 | ONE_TOUCH_FILTER = auto() 159 | OPERATION_MODE = auto() 160 | OPTIMAL_HUMIDITY = auto() 161 | OVEN_OPERATION_MODE = auto() 162 | OVEN_TYPE = auto() 163 | PERSONALIZATION_MODE = auto() 164 | PM1 = auto() 165 | PM10 = auto() 166 | PM2 = auto() 167 | POWER_LEVEL = auto() 168 | POWER_SAVE_ENABLED = auto() 169 | RAPID_FREEZE = auto() 170 | RECIPE_NAME = auto() 171 | RELATIVE_HOUR_TO_START = auto() 172 | RELATIVE_HOUR_TO_STOP = auto() 173 | RELATIVE_MINUTE_TO_START = auto() 174 | RELATIVE_MINUTE_TO_STOP = auto() 175 | REMAIN_HOUR = auto() 176 | REMAIN_MINUTE = auto() 177 | REMAIN_SECOND = auto() 178 | REMOTE_CONTROL_ENABLED = auto() 179 | RINSE_LEVEL = auto() 180 | RINSE_REFILL = auto() 181 | ROOM_TEMP_MODE = auto() 182 | ROOM_WATER_MODE = auto() 183 | ROOM_AIR_COOL_MAX_TEMPERATURE_C = auto() 184 | ROOM_AIR_COOL_MAX_TEMPERATURE_F = auto() 185 | ROOM_AIR_COOL_MIN_TEMPERATURE_C = auto() 186 | ROOM_AIR_COOL_MIN_TEMPERATURE_F = auto() 187 | ROOM_AIR_COOL_TARGET_TEMPERATURE_C = auto() 188 | ROOM_AIR_COOL_TARGET_TEMPERATURE_F = auto() 189 | ROOM_AIR_CURRENT_TEMPERATURE_C = auto() 190 | ROOM_AIR_CURRENT_TEMPERATURE_F = auto() 191 | ROOM_AIR_HEAT_MAX_TEMPERATURE_C = auto() 192 | ROOM_AIR_HEAT_MAX_TEMPERATURE_F = auto() 193 | ROOM_AIR_HEAT_MIN_TEMPERATURE_C = auto() 194 | ROOM_AIR_HEAT_MIN_TEMPERATURE_F = auto() 195 | ROOM_AIR_HEAT_TARGET_TEMPERATURE_C = auto() 196 | ROOM_AIR_HEAT_TARGET_TEMPERATURE_F = auto() 197 | ROOM_CURRENT_TEMPERATURE_C = auto() 198 | ROOM_CURRENT_TEMPERATURE_F = auto() 199 | ROOM_IN_WATER_CURRENT_TEMPERATURE_C = auto() 200 | ROOM_IN_WATER_CURRENT_TEMPERATURE_F = auto() 201 | ROOM_OUT_WATER_CURRENT_TEMPERATURE_C = auto() 202 | ROOM_OUT_WATER_CURRENT_TEMPERATURE_F = auto() 203 | ROOM_TARGET_TEMPERATURE_C = auto() 204 | ROOM_TARGET_TEMPERATURE_F = auto() 205 | ROOM_TEMPERATURE_UNIT = auto() 206 | ROOM_WATER_COOL_MAX_TEMPERATURE_C = auto() 207 | ROOM_WATER_COOL_MAX_TEMPERATURE_F = auto() 208 | ROOM_WATER_COOL_MIN_TEMPERATURE_C = auto() 209 | ROOM_WATER_COOL_MIN_TEMPERATURE_F = auto() 210 | ROOM_WATER_COOL_TARGET_TEMPERATURE_C = auto() 211 | ROOM_WATER_COOL_TARGET_TEMPERATURE_F = auto() 212 | ROOM_WATER_HEAT_MAX_TEMPERATURE_C = auto() 213 | ROOM_WATER_HEAT_MAX_TEMPERATURE_F = auto() 214 | ROOM_WATER_HEAT_MIN_TEMPERATURE_C = auto() 215 | ROOM_WATER_HEAT_MIN_TEMPERATURE_F = auto() 216 | ROOM_WATER_HEAT_TARGET_TEMPERATURE_C = auto() 217 | ROOM_WATER_HEAT_TARGET_TEMPERATURE_F = auto() 218 | RUNNING_HOUR = auto() 219 | RUNNING_MINUTE = auto() 220 | SABBATH_MODE = auto() 221 | SIGNAL_LEVEL = auto() 222 | SLEEP_MODE = auto() 223 | SLEEP_TIMER_RELATIVE_HOUR_TO_STOP = auto() 224 | SLEEP_TIMER_RELATIVE_MINUTE_TO_STOP = auto() 225 | SOFTENING_LEVEL = auto() 226 | START_HOUR = auto() 227 | START_MINUTE = auto() 228 | STERILIZING_STATE = auto() 229 | STYLER_OPERATION_MODE = auto() 230 | TARGET_HOUR = auto() 231 | TARGET_HUMIDITY = auto() 232 | TARGET_MINUTE = auto() 233 | TARGET_SECOND = auto() 234 | TARGET_TEMPERATURE = auto() 235 | TARGET_TEMPERATURE_C = auto() 236 | TARGET_TEMPERATURE_F = auto() 237 | TEMPERATURE = auto() 238 | TEMPERATURE_STATE = auto() 239 | TEMPERATURE_UNIT = auto() 240 | TIMER_HOUR = auto() 241 | TIMER_MINUTE = auto() 242 | TIMER_SECOND = auto() 243 | TOTAL_HOUR = auto() 244 | TOTAL_MINUTE = auto() 245 | TOTAL_POLLUTION = auto() 246 | TOTAL_POLLUTION_LEVEL = auto() 247 | TWO_SET_ENABLED = auto() 248 | TWO_SET_COOL_TARGET_TEMPERATURE = auto() 249 | TWO_SET_COOL_TARGET_TEMPERATURE_C = auto() 250 | TWO_SET_COOL_TARGET_TEMPERATURE_F = auto() 251 | TWO_SET_CURRENT_TEMPERATURE = auto() 252 | TWO_SET_CURRENT_TEMPERATURE_C = auto() 253 | TWO_SET_CURRENT_TEMPERATURE_F = auto() 254 | TWO_SET_HEAT_TARGET_TEMPERATURE = auto() 255 | TWO_SET_HEAT_TARGET_TEMPERATURE_C = auto() 256 | TWO_SET_HEAT_TARGET_TEMPERATURE_F = auto() 257 | TWO_SET_TEMPERATURE_UNIT = auto() 258 | USED_TIME = auto() 259 | UV_NANO = auto() 260 | WARM_MODE = auto() 261 | WASHER_OPERATION_MODE = auto() 262 | WASHER_MODE = auto() 263 | WATER_FILTER_INFO_UNIT = auto() 264 | WATER_HEATER_OPERATION_MODE = auto() 265 | WATER_TYPE = auto() 266 | WIND_ANGLE = auto() 267 | WIND_ROTATE_LEFT_RIGHT = auto() 268 | WIND_ROTATE_UP_DOWN = auto() 269 | WIND_STEP = auto() 270 | WIND_STRENGTH = auto() 271 | WIND_TEMPERATURE = auto() 272 | WIND_VOLUME = auto() 273 | WORT_INFO = auto() 274 | VENTILATOR_OPERATION_MODE = auto() 275 | YEAST_INFO = auto() 276 | 277 | 278 | class Location(StrEnum): 279 | CENTER = auto() 280 | CENTER_FRONT = auto() 281 | CENTER_REAR = auto() 282 | LEFT_FRONT = auto() 283 | LEFT_REAR = auto() 284 | RIGHT_FRONT = auto() 285 | RIGHT_REAR = auto() 286 | BURNER_1 = auto() 287 | BURNER_2 = auto() 288 | BURNER_3 = auto() 289 | BURNER_4 = auto() 290 | BURNER_5 = auto() 291 | BURNER_6 = auto() 292 | BURNER_7 = auto() 293 | BURNER_8 = auto() 294 | INDUCTION_1 = auto() 295 | INDUCTION_2 = auto() 296 | SOUSVIDE_1 = auto() 297 | 298 | TOP = auto() 299 | MIDDLE = auto() 300 | BOTTOM = auto() 301 | LEFT = auto() 302 | RIGHT = auto() 303 | SINGLE = auto() 304 | 305 | OVEN = auto() 306 | UPPER = auto() 307 | LOWER = auto() 308 | 309 | MAIN = auto() 310 | MINI = auto() 311 | 312 | FRIDGE = auto() 313 | FREEZER = auto() 314 | CONVERTIBLE = auto() 315 | 316 | DRYER = auto() 317 | WASHER = auto() 318 | -------------------------------------------------------------------------------- /thinqconnect/devices/cooktop.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """ 4 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 5 | * SPDX-License-Identifier: Apache-2.0 6 | """ 7 | from typing import Any 8 | 9 | from ..thinq_api import ThinQApi 10 | from .connect_device import ConnectDeviceProfile, ConnectMainDevice, ConnectSubDevice 11 | from .const import Location, Property, Resource 12 | 13 | 14 | class CooktopSubProfile(ConnectDeviceProfile): 15 | def __init__(self, profile: dict[str, Any], location_name: Location): 16 | self._location_name = location_name 17 | super().__init__( 18 | profile=profile, 19 | resource_map={ 20 | "cookingZone": Resource.COOKING_ZONE, 21 | "power": Resource.POWER, 22 | "remoteControlEnable": Resource.REMOTE_CONTROL_ENABLE, 23 | "timer": Resource.TIMER, 24 | }, 25 | profile_map={ 26 | "cookingZone": { 27 | "currentState": Property.CURRENT_STATE, 28 | }, 29 | "power": { 30 | "powerLevel": Property.POWER_LEVEL, 31 | }, 32 | "remoteControlEnable": { 33 | "remoteControlEnabled": Property.REMOTE_CONTROL_ENABLED, 34 | }, 35 | "timer": { 36 | "remainHour": Property.REMAIN_HOUR, 37 | "remainMinute": Property.REMAIN_MINUTE, 38 | }, 39 | }, 40 | ) 41 | 42 | def generate_properties(self, property: dict[str, Any]) -> None: 43 | """Get properties.""" 44 | for location_property in property: 45 | if location_property.get("location", {}).get("locationName") != self._location_name: 46 | continue 47 | super().generate_properties(location_property) 48 | 49 | 50 | class CooktopProfile(ConnectDeviceProfile): 51 | def __init__(self, profile: dict[str, Any]): 52 | use_extension_property = True if "extensionProperty" in profile else False 53 | resource_map = {"operation": Resource.OPERATION} if use_extension_property else {} 54 | profile_map = {"operation": {"operationMode": Property.OPERATION_MODE}} if use_extension_property else {} 55 | super().__init__( 56 | profile=profile, 57 | resource_map=resource_map, 58 | profile_map=profile_map, 59 | location_map={ 60 | "CENTER": Location.CENTER, 61 | "CENTER_FRONT": Location.CENTER_FRONT, 62 | "CENTER_REAR": Location.CENTER_REAR, 63 | "LEFT_FRONT": Location.LEFT_FRONT, 64 | "LEFT_REAR": Location.LEFT_REAR, 65 | "RIGHT_FRONT": Location.RIGHT_FRONT, 66 | "RIGHT_REAR": Location.RIGHT_REAR, 67 | "BURNER_1": Location.BURNER_1, 68 | "BURNER_2": Location.BURNER_2, 69 | "BURNER_3": Location.BURNER_3, 70 | "BURNER_4": Location.BURNER_4, 71 | "BURNER_5": Location.BURNER_5, 72 | "BURNER_6": Location.BURNER_6, 73 | "BURNER_7": Location.BURNER_7, 74 | "BURNER_8": Location.BURNER_8, 75 | "INDUCTION_1": Location.INDUCTION_1, 76 | "INDUCTION_2": Location.INDUCTION_2, 77 | "SOUSVIDE_1": Location.SOUSVIDE_1, 78 | }, 79 | use_extension_property=use_extension_property, 80 | ) 81 | 82 | for profile_property in profile.get("property", []): 83 | location_name = profile_property.get("location", {}).get("locationName") 84 | if location_name in self._LOCATION_MAP.keys(): 85 | attr_key = self._LOCATION_MAP[location_name] 86 | _sub_profile = CooktopSubProfile(profile, location_name) 87 | self._set_sub_profile(attr_key, _sub_profile) 88 | self._set_location_properties(attr_key, _sub_profile.properties) 89 | 90 | 91 | class CooktopSubDevice(ConnectSubDevice): 92 | """Cooktop Device Sub.""" 93 | 94 | def __init__( 95 | self, 96 | profiles: CooktopSubProfile, 97 | location_name: Location, 98 | thinq_api: ThinQApi, 99 | device_id: str, 100 | device_type: str, 101 | model_name: str, 102 | alias: str, 103 | reportable: bool, 104 | ): 105 | super().__init__(profiles, location_name, thinq_api, device_id, device_type, model_name, alias, reportable) 106 | 107 | @property 108 | def profiles(self) -> CooktopSubProfile: 109 | return self._profiles 110 | 111 | def _get_command_payload(self): 112 | return { 113 | "power": {"powerLevel": self.get_status(Property.POWER_LEVEL)}, 114 | "timer": { 115 | "remainHour": self.get_status(Property.REMAIN_HOUR), 116 | "remainMinute": self.get_status(Property.REMAIN_MINUTE), 117 | }, 118 | "location": {"locationName": self.location_name}, 119 | } 120 | 121 | async def _do_custom_range_attribute_command(self, attr_name: str, value: int) -> dict | None: 122 | full_payload: dict[str, dict[str, int | str]] = self._get_command_payload() 123 | payload = self.profiles.get_range_attribute_payload(attr_name, value) 124 | for resource in payload.keys(): 125 | full_payload[resource].update(payload[resource]) 126 | return await self.thinq_api.async_post_device_control(device_id=self.device_id, payload=full_payload) 127 | 128 | async def set_power_level(self, value: int) -> dict | None: 129 | return await self._do_custom_range_attribute_command(Property.POWER_LEVEL, value) 130 | 131 | async def set_remain_hour(self, value: int) -> dict | None: 132 | return await self._do_custom_range_attribute_command(Property.REMAIN_HOUR, value) 133 | 134 | async def set_remain_minute(self, value: int) -> dict | None: 135 | return await self._do_custom_range_attribute_command(Property.REMAIN_MINUTE, value) 136 | 137 | 138 | class CooktopDevice(ConnectMainDevice): 139 | """Cooktop Property.""" 140 | 141 | def __init__( 142 | self, 143 | thinq_api: ThinQApi, 144 | device_id: str, 145 | device_type: str, 146 | model_name: str, 147 | alias: str, 148 | reportable: bool, 149 | profile: dict[str, Any], 150 | ): 151 | self._sub_devices: dict[str, CooktopSubDevice] = {} 152 | super().__init__( 153 | thinq_api=thinq_api, 154 | device_id=device_id, 155 | device_type=device_type, 156 | model_name=model_name, 157 | alias=alias, 158 | reportable=reportable, 159 | profiles=CooktopProfile(profile=profile), 160 | sub_device_type=CooktopSubDevice, 161 | ) 162 | 163 | @property 164 | def profiles(self) -> CooktopProfile: 165 | return self._profiles 166 | 167 | def get_sub_device(self, location_name: Location) -> CooktopSubDevice: 168 | return super().get_sub_device(location_name) 169 | 170 | async def set_operation_mode(self, mode: str) -> dict | None: 171 | return await self.do_enum_attribute_command(Property.OPERATION_MODE, mode) 172 | 173 | async def set_power_level(self, location_name: Location, value: int) -> dict | None: 174 | if sub_device := self._sub_devices.get(location_name): 175 | return await sub_device.set_power_level(value) 176 | else: 177 | raise ValueError(f"Invalid location : {location_name}") 178 | 179 | async def set_remain_hour(self, location_name: Location, value: int) -> dict | None: 180 | if sub_device := self._sub_devices.get(location_name): 181 | return await sub_device.set_remain_hour(value) 182 | else: 183 | raise ValueError(f"Invalid location : {location_name}") 184 | 185 | async def set_remain_minute(self, location_name: Location, value: int) -> dict | None: 186 | if sub_device := self._sub_devices.get(location_name): 187 | return await sub_device.set_remain_minute(value) 188 | else: 189 | raise ValueError(f"Invalid location : {location_name}") 190 | -------------------------------------------------------------------------------- /thinqconnect/devices/dehumidifier.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """ 4 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 5 | * SPDX-License-Identifier: Apache-2.0 6 | """ 7 | from typing import Any 8 | 9 | from ..thinq_api import ThinQApi 10 | from .connect_device import ConnectBaseDevice, ConnectDeviceProfile 11 | from .const import Property, Resource 12 | 13 | 14 | class DehumidifierProfile(ConnectDeviceProfile): 15 | def __init__(self, profile: dict[str, Any]): 16 | super().__init__( 17 | profile=profile, 18 | resource_map={ 19 | "operation": Resource.OPERATION, 20 | "dehumidifierJobMode": Resource.DEHUMIDIFIER_JOB_MODE, 21 | "humidity": Resource.HUMIDITY, 22 | "airFlow": Resource.AIR_FLOW, 23 | }, 24 | profile_map={ 25 | "operation": {"dehumidifierOperationMode": Property.DEHUMIDIFIER_OPERATION_MODE}, 26 | "dehumidifierJobMode": {"currentJobMode": Property.CURRENT_JOB_MODE}, 27 | "humidity": {"currentHumidity": Property.CURRENT_HUMIDITY}, 28 | "airFlow": {"windStrength": Property.WIND_STRENGTH}, 29 | }, 30 | ) 31 | 32 | 33 | class DehumidifierDevice(ConnectBaseDevice): 34 | def __init__( 35 | self, 36 | thinq_api: ThinQApi, 37 | device_id: str, 38 | device_type: str, 39 | model_name: str, 40 | alias: str, 41 | reportable: bool, 42 | profile: dict[str, Any], 43 | ): 44 | super().__init__( 45 | thinq_api=thinq_api, 46 | device_id=device_id, 47 | device_type=device_type, 48 | model_name=model_name, 49 | alias=alias, 50 | reportable=reportable, 51 | profiles=DehumidifierProfile(profile=profile), 52 | ) 53 | 54 | @property 55 | def profiles(self) -> DehumidifierProfile: 56 | return self._profiles 57 | 58 | async def set_dehumidifier_operation_mode(self, mode: str) -> dict | None: 59 | return await self.do_enum_attribute_command(Property.DEHUMIDIFIER_OPERATION_MODE, mode) 60 | 61 | async def set_current_job_mode(self, job_mode: str) -> dict | None: 62 | return await self.do_enum_attribute_command(Property.CURRENT_JOB_MODE, job_mode) 63 | 64 | async def set_wind_strength(self, wind_strength: str) -> dict | None: 65 | return await self.do_enum_attribute_command(Property.WIND_STRENGTH, wind_strength) 66 | -------------------------------------------------------------------------------- /thinqconnect/devices/dish_washer.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """ 4 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 5 | * SPDX-License-Identifier: Apache-2.0 6 | """ 7 | from typing import Any 8 | 9 | from ..thinq_api import ThinQApi 10 | from .connect_device import ConnectBaseDevice, ConnectDeviceProfile 11 | from .const import Property, Resource 12 | 13 | 14 | class DishWasherProfile(ConnectDeviceProfile): 15 | def __init__(self, profile: dict[str, Any]): 16 | super().__init__( 17 | profile=profile, 18 | resource_map={ 19 | "runState": Resource.RUN_STATE, 20 | "dishWashingStatus": Resource.DISH_WASHING_STATUS, 21 | "preference": Resource.PREFERENCE, 22 | "doorStatus": Resource.DOOR_STATUS, 23 | "operation": Resource.OPERATION, 24 | "remoteControlEnable": Resource.REMOTE_CONTROL_ENABLE, 25 | "timer": Resource.TIMER, 26 | "dishWashingCourse": Resource.DISH_WASHING_COURSE, 27 | }, 28 | profile_map={ 29 | "runState": {"currentState": Property.CURRENT_STATE}, 30 | "dishWashingStatus": {"rinseRefill": Property.RINSE_REFILL}, 31 | "preference": { 32 | "rinseLevel": Property.RINSE_LEVEL, 33 | "softeningLevel": Property.SOFTENING_LEVEL, 34 | "mCReminder": Property.MACHINE_CLEAN_REMINDER, 35 | "signalLevel": Property.SIGNAL_LEVEL, 36 | "cleanLReminder": Property.CLEAN_LIGHT_REMINDER, 37 | }, 38 | "doorStatus": {"doorState": Property.DOOR_STATE}, 39 | "operation": {"dishWasherOperationMode": Property.DISH_WASHER_OPERATION_MODE}, 40 | "remoteControlEnable": {"remoteControlEnabled": Property.REMOTE_CONTROL_ENABLED}, 41 | "timer": { 42 | "relativeHourToStart": Property.RELATIVE_HOUR_TO_START, 43 | "relativeMinuteToStart": Property.RELATIVE_MINUTE_TO_START, 44 | "remainHour": Property.REMAIN_HOUR, 45 | "remainMinute": Property.REMAIN_MINUTE, 46 | "totalHour": Property.TOTAL_HOUR, 47 | "totalMinute": Property.TOTAL_MINUTE, 48 | }, 49 | "dishWashingCourse": {"currentDishWashingCourse": Property.CURRENT_DISH_WASHING_COURSE}, 50 | }, 51 | ) 52 | 53 | 54 | class DishWasherDevice(ConnectBaseDevice): 55 | """DishWasher Property.""" 56 | 57 | def __init__( 58 | self, 59 | thinq_api: ThinQApi, 60 | device_id: str, 61 | device_type: str, 62 | model_name: str, 63 | alias: str, 64 | reportable: bool, 65 | profile: dict[str, Any], 66 | ): 67 | super().__init__( 68 | thinq_api=thinq_api, 69 | device_id=device_id, 70 | device_type=device_type, 71 | model_name=model_name, 72 | alias=alias, 73 | reportable=reportable, 74 | profiles=DishWasherProfile(profile=profile), 75 | ) 76 | 77 | @property 78 | def profiles(self) -> DishWasherProfile: 79 | return self._profiles 80 | 81 | async def set_dish_washer_operation_mode(self, mode: str) -> dict | None: 82 | return await self.do_enum_attribute_command(Property.DISH_WASHER_OPERATION_MODE, mode) 83 | 84 | async def set_relative_hour_to_start(self, hour: int) -> dict | None: 85 | return await self.do_attribute_command(Property.RELATIVE_HOUR_TO_START, hour) 86 | -------------------------------------------------------------------------------- /thinqconnect/devices/dryer.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """ 4 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 5 | * SPDX-License-Identifier: Apache-2.0 6 | """ 7 | from typing import Any 8 | 9 | from ..thinq_api import ThinQApi 10 | from .connect_device import ConnectBaseDevice, ConnectDeviceProfile 11 | from .const import Property, Resource 12 | 13 | 14 | class DryerProfile(ConnectDeviceProfile): 15 | def __init__(self, profile: dict[str, Any]): 16 | super().__init__( 17 | profile=profile, 18 | resource_map={ 19 | "runState": Resource.RUN_STATE, 20 | "operation": Resource.OPERATION, 21 | "remoteControlEnable": Resource.REMOTE_CONTROL_ENABLE, 22 | "timer": Resource.TIMER, 23 | }, 24 | profile_map={ 25 | "runState": {"currentState": Property.CURRENT_STATE}, 26 | "operation": { 27 | "dryerOperationMode": Property.DRYER_OPERATION_MODE, 28 | }, 29 | "remoteControlEnable": {"remoteControlEnabled": Property.REMOTE_CONTROL_ENABLED}, 30 | "timer": { 31 | "remainHour": Property.REMAIN_HOUR, 32 | "remainMinute": Property.REMAIN_MINUTE, 33 | "totalHour": Property.TOTAL_HOUR, 34 | "totalMinute": Property.TOTAL_MINUTE, 35 | "relativeHourToStop": Property.RELATIVE_HOUR_TO_STOP, 36 | "relativeMinuteToStop": Property.RELATIVE_MINUTE_TO_STOP, 37 | "relativeHourToStart": Property.RELATIVE_HOUR_TO_START, 38 | "relativeMinuteToStart": Property.RELATIVE_MINUTE_TO_START, 39 | }, 40 | }, 41 | ) 42 | 43 | 44 | class DryerDevice(ConnectBaseDevice): 45 | """Dryer Property.""" 46 | 47 | def __init__( 48 | self, 49 | thinq_api: ThinQApi, 50 | device_id: str, 51 | device_type: str, 52 | model_name: str, 53 | alias: str, 54 | reportable: bool, 55 | profile: dict[str, Any], 56 | ): 57 | super().__init__( 58 | thinq_api=thinq_api, 59 | device_id=device_id, 60 | device_type=device_type, 61 | model_name=model_name, 62 | alias=alias, 63 | reportable=reportable, 64 | profiles=DryerProfile(profile=profile), 65 | ) 66 | 67 | @property 68 | def profiles(self) -> DryerProfile: 69 | return self._profiles 70 | 71 | async def set_dryer_operation_mode(self, mode: str) -> dict | None: 72 | return await self.do_enum_attribute_command(Property.DRYER_OPERATION_MODE, mode) 73 | 74 | async def set_relative_hour_to_start(self, hour: int) -> dict | None: 75 | return await self.do_attribute_command(Property.RELATIVE_HOUR_TO_START, hour) 76 | 77 | async def set_relative_hour_to_stop(self, hour: int) -> dict | None: 78 | return await self.do_range_attribute_command(Property.RELATIVE_HOUR_TO_STOP, hour) 79 | -------------------------------------------------------------------------------- /thinqconnect/devices/home_brew.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """ 4 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 5 | * SPDX-License-Identifier: Apache-2.0 6 | """ 7 | from typing import Any 8 | 9 | from ..thinq_api import ThinQApi 10 | from .connect_device import ConnectBaseDevice, ConnectDeviceProfile 11 | from .const import Property, Resource 12 | 13 | 14 | class HomeBrewProfile(ConnectDeviceProfile): 15 | def __init__(self, profile: dict[str, Any]): 16 | super().__init__( 17 | profile=profile, 18 | resource_map={"runState": Resource.RUN_STATE, "recipe": Resource.RECIPE, "timer": Resource.TIMER}, 19 | profile_map={ 20 | "runState": {"currentState": Property.CURRENT_STATE}, 21 | "recipe": { 22 | "beerRemain": Property.BEER_REMAIN, 23 | "flavorInfo": Property.FLAVOR_INFO, 24 | "flavorCapsule1": Property.FLAVOR_CAPSULE_1, 25 | "flavorCapsule2": Property.FLAVOR_CAPSULE_2, 26 | "hopOilInfo": Property.HOP_OIL_INFO, 27 | "hopOilCapsule1": Property.HOP_OIL_CAPSULE_1, 28 | "hopOilCapsule2": Property.HOP_OIL_CAPSULE_2, 29 | "wortInfo": Property.WORT_INFO, 30 | "yeastInfo": Property.YEAST_INFO, 31 | "recipeName": Property.RECIPE_NAME, 32 | }, 33 | "timer": { 34 | "elapsedDayState": Property.ELAPSED_DAY_STATE, 35 | "elapsedDayTotal": Property.ELAPSED_DAY_TOTAL, 36 | }, 37 | }, 38 | ) 39 | 40 | 41 | class HomeBrewDevice(ConnectBaseDevice): 42 | """HomeBrew Property.""" 43 | 44 | def __init__( 45 | self, 46 | thinq_api: ThinQApi, 47 | device_id: str, 48 | device_type: str, 49 | model_name: str, 50 | alias: str, 51 | reportable: bool, 52 | profile: dict[str, Any], 53 | ): 54 | super().__init__( 55 | thinq_api=thinq_api, 56 | device_id=device_id, 57 | device_type=device_type, 58 | model_name=model_name, 59 | alias=alias, 60 | reportable=reportable, 61 | profiles=HomeBrewProfile(profile=profile), 62 | ) 63 | 64 | @property 65 | def profiles(self) -> HomeBrewProfile: 66 | return self._profiles 67 | -------------------------------------------------------------------------------- /thinqconnect/devices/hood.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """ 4 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 5 | * SPDX-License-Identifier: Apache-2.0 6 | """ 7 | from typing import Any 8 | 9 | from ..thinq_api import ThinQApi 10 | from .connect_device import ConnectBaseDevice, ConnectDeviceProfile 11 | from .const import Property, Resource 12 | 13 | 14 | class HoodProfile(ConnectDeviceProfile): 15 | def __init__(self, profile: dict[str, Any]): 16 | super().__init__( 17 | profile=profile, 18 | resource_map={ 19 | "ventilation": Resource.VENTILATION, 20 | "lamp": Resource.LAMP, 21 | "operation": Resource.OPERATION, 22 | }, 23 | profile_map={ 24 | "ventilation": { 25 | "fanSpeed": Property.FAN_SPEED, 26 | }, 27 | "lamp": { 28 | "lampBrightness": Property.LAMP_BRIGHTNESS, 29 | }, 30 | "operation": { 31 | "hoodOperationMode": Property.HOOD_OPERATION_MODE, 32 | }, 33 | }, 34 | ) 35 | 36 | 37 | class HoodDevice(ConnectBaseDevice): 38 | """Oven Property.""" 39 | 40 | def __init__( 41 | self, 42 | thinq_api: ThinQApi, 43 | device_id: str, 44 | device_type: str, 45 | model_name: str, 46 | alias: str, 47 | reportable: bool, 48 | profile: dict[str, Any], 49 | ): 50 | super().__init__( 51 | thinq_api=thinq_api, 52 | device_id=device_id, 53 | device_type=device_type, 54 | model_name=model_name, 55 | alias=alias, 56 | reportable=reportable, 57 | profiles=HoodProfile(profile=profile), 58 | ) 59 | 60 | @property 61 | def profiles(self) -> HoodProfile: 62 | return self._profiles 63 | 64 | async def set_fan_speed_lamp_brightness(self, fan_speed: int, lamp_brightness: int) -> dict | None: 65 | return await self.do_multi_range_attribute_command( 66 | { 67 | Property.FAN_SPEED: fan_speed, 68 | Property.LAMP_BRIGHTNESS: lamp_brightness, 69 | } 70 | ) 71 | 72 | async def set_fan_speed(self, fan_speed: int) -> dict | None: 73 | return await self.do_multi_range_attribute_command( 74 | {Property.FAN_SPEED: fan_speed, Property.LAMP_BRIGHTNESS: self.lamp_brightness} 75 | ) 76 | 77 | async def set_lamp_brightness(self, lamp_brightness: int) -> dict | None: 78 | return await self.do_multi_range_attribute_command( 79 | { 80 | Property.FAN_SPEED: self.fan_speed, 81 | Property.LAMP_BRIGHTNESS: lamp_brightness, 82 | } 83 | ) 84 | -------------------------------------------------------------------------------- /thinqconnect/devices/humidifier.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """ 4 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 5 | * SPDX-License-Identifier: Apache-2.0 6 | """ 7 | from typing import Any 8 | 9 | from ..thinq_api import ThinQApi 10 | from .connect_device import ConnectBaseDevice, ConnectDeviceProfile 11 | from .const import Property, Resource 12 | 13 | 14 | class HumidifierProfile(ConnectDeviceProfile): 15 | def __init__(self, profile: dict[str, Any]): 16 | super().__init__( 17 | profile=profile, 18 | resource_map={ 19 | "humidifierJobMode": Resource.HUMIDIFIER_JOB_MODE, 20 | "operation": Resource.OPERATION, 21 | "timer": Resource.TIMER, 22 | "sleepTimer": Resource.SLEEP_TIMER, 23 | "humidity": Resource.HUMIDITY, 24 | "airFlow": Resource.AIR_FLOW, 25 | "airQualitySensor": Resource.AIR_QUALITY_SENSOR, 26 | "display": Resource.DISPLAY, 27 | "moodLamp": Resource.MOOD_LAMP, 28 | }, 29 | profile_map={ 30 | "humidifierJobMode": { 31 | "currentJobMode": Property.CURRENT_JOB_MODE, 32 | }, 33 | "operation": { 34 | "humidifierOperationMode": Property.HUMIDIFIER_OPERATION_MODE, 35 | "autoMode": Property.AUTO_MODE, 36 | "sleepMode": Property.SLEEP_MODE, 37 | "hygieneDryMode": Property.HYGIENE_DRY_MODE, 38 | }, 39 | "timer": { 40 | "absoluteHourToStart": Property.ABSOLUTE_HOUR_TO_START, 41 | "absoluteHourToStop": Property.ABSOLUTE_HOUR_TO_STOP, 42 | "absoluteMinuteToStart": Property.ABSOLUTE_MINUTE_TO_START, 43 | "absoluteMinuteToStop": Property.ABSOLUTE_MINUTE_TO_STOP, 44 | }, 45 | "sleepTimer": { 46 | "relativeHourToStop": Property.SLEEP_TIMER_RELATIVE_HOUR_TO_STOP, 47 | "relativeMinuteToStop": Property.SLEEP_TIMER_RELATIVE_MINUTE_TO_STOP, 48 | }, 49 | "humidity": { 50 | "targetHumidity": Property.TARGET_HUMIDITY, 51 | "warmMode": Property.WARM_MODE, 52 | }, 53 | "airFlow": { 54 | "windStrength": Property.WIND_STRENGTH, 55 | }, 56 | "airQualitySensor": { 57 | "monitoringEnabled": Property.MONITORING_ENABLED, 58 | "totalPollution": Property.TOTAL_POLLUTION, 59 | "totalPollutionLevel": Property.TOTAL_POLLUTION_LEVEL, 60 | "PM1": Property.PM1, 61 | "PM2": Property.PM2, 62 | "PM10": Property.PM10, 63 | "humidity": Property.HUMIDITY, 64 | "temperature": Property.TEMPERATURE, 65 | }, 66 | "display": { 67 | "light": Property.DISPLAY_LIGHT, 68 | }, 69 | "moodLamp": { 70 | "moodLampState": Property.MOOD_LAMP_STATE, 71 | }, 72 | }, 73 | ) 74 | 75 | 76 | class HumidifierDevice(ConnectBaseDevice): 77 | _CUSTOM_SET_PROPERTY_NAME = { 78 | Property.ABSOLUTE_HOUR_TO_START: "absolute_time_to_start", 79 | Property.ABSOLUTE_MINUTE_TO_START: "absolute_time_to_start", 80 | Property.ABSOLUTE_HOUR_TO_STOP: "absolute_time_to_stop", 81 | Property.ABSOLUTE_MINUTE_TO_STOP: "absolute_time_to_stop", 82 | Property.SLEEP_TIMER_RELATIVE_HOUR_TO_STOP: "sleep_timer_relative_time_to_stop", 83 | Property.SLEEP_TIMER_RELATIVE_MINUTE_TO_STOP: "sleep_timer_relative_time_to_stop", 84 | } 85 | 86 | def __init__( 87 | self, 88 | thinq_api: ThinQApi, 89 | device_id: str, 90 | device_type: str, 91 | model_name: str, 92 | alias: str, 93 | reportable: bool, 94 | profile: dict[str, Any], 95 | ): 96 | super().__init__( 97 | thinq_api=thinq_api, 98 | device_id=device_id, 99 | device_type=device_type, 100 | model_name=model_name, 101 | alias=alias, 102 | reportable=reportable, 103 | profiles=HumidifierProfile(profile=profile), 104 | ) 105 | 106 | @property 107 | def profiles(self) -> HumidifierProfile: 108 | return self._profiles 109 | 110 | async def set_current_job_mode(self, job_mode: str) -> dict | None: 111 | return await self.do_enum_attribute_command(Property.CURRENT_JOB_MODE, job_mode) 112 | 113 | async def set_humidifier_operation_mode(self, operation_mode: str) -> dict | None: 114 | return await self.do_enum_attribute_command(Property.HUMIDIFIER_OPERATION_MODE, operation_mode) 115 | 116 | async def set_auto_mode(self, auto_mode: str) -> dict | None: 117 | return await self.do_enum_attribute_command(Property.AUTO_MODE, auto_mode) 118 | 119 | async def set_sleep_mode(self, sleep_mode: str) -> dict | None: 120 | return await self.do_enum_attribute_command(Property.SLEEP_MODE, sleep_mode) 121 | 122 | async def set_hygiene_dry_mode(self, hygiene_dry_mode: str) -> dict | None: 123 | return await self.do_enum_attribute_command(Property.HYGIENE_DRY_MODE, hygiene_dry_mode) 124 | 125 | async def set_absolute_time_to_start(self, hour: int, minute: int) -> dict | None: 126 | return await self.do_multi_attribute_command( 127 | { 128 | Property.ABSOLUTE_HOUR_TO_START: hour, 129 | Property.ABSOLUTE_MINUTE_TO_START: minute, 130 | } 131 | ) 132 | 133 | async def set_absolute_time_to_stop(self, hour: int, minute: int) -> dict | None: 134 | return await self.do_multi_attribute_command( 135 | { 136 | Property.ABSOLUTE_HOUR_TO_STOP: hour, 137 | Property.ABSOLUTE_MINUTE_TO_STOP: minute, 138 | } 139 | ) 140 | 141 | async def set_sleep_timer_relative_time_to_stop(self, hour: int, minute: int = 0) -> dict | None: 142 | return await self.do_multi_attribute_command( 143 | { 144 | Property.SLEEP_TIMER_RELATIVE_HOUR_TO_STOP: hour, 145 | **({Property.SLEEP_TIMER_RELATIVE_MINUTE_TO_STOP: minute} if minute != 0 else {}), 146 | } 147 | ) 148 | 149 | async def set_target_humidity(self, target_humidity: int) -> dict | None: 150 | return await self.do_attribute_command(Property.TARGET_HUMIDITY, target_humidity) 151 | 152 | async def set_warm_mode(self, warm_mode: str) -> dict | None: 153 | return await self.do_enum_attribute_command(Property.WARM_MODE, warm_mode) 154 | 155 | async def set_wind_strength(self, wind_strength: str) -> dict | None: 156 | return await self.do_enum_attribute_command(Property.WIND_STRENGTH, wind_strength) 157 | 158 | async def set_display_light(self, display_light: str) -> dict | None: 159 | return await self.do_enum_attribute_command(Property.DISPLAY_LIGHT, display_light) 160 | 161 | async def set_mood_lamp_state(self, state: str) -> dict | None: 162 | return await self.do_enum_attribute_command(Property.MOOD_LAMP_STATE, state) 163 | -------------------------------------------------------------------------------- /thinqconnect/devices/kimchi_refrigerator.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """ 4 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 5 | * SPDX-License-Identifier: Apache-2.0 6 | """ 7 | from typing import Any 8 | 9 | from ..thinq_api import ThinQApi 10 | from .connect_device import ( 11 | READABLE_VALUES, 12 | WRITABLE_VALUES, 13 | ConnectDeviceProfile, 14 | ConnectMainDevice, 15 | ConnectSubDevice, 16 | ConnectSubDeviceProfile, 17 | ) 18 | from .const import Location, Property, Resource 19 | 20 | 21 | class KimchiRefrigeratorSubProfile(ConnectSubDeviceProfile): 22 | def __init__(self, profile: dict[str, Any], location_name: Location): 23 | super().__init__( 24 | profile=profile, 25 | location_name=location_name, 26 | resource_map={ 27 | "temperature": Resource.TEMPERATURE, 28 | }, 29 | profile_map={ 30 | "temperature": { 31 | "targetTemperature": Property.TARGET_TEMPERATURE, 32 | }, 33 | }, 34 | custom_resources=["temperature"], 35 | ) 36 | 37 | def _generate_custom_resource_properties( 38 | self, resource_key: str, resource_property: dict | list, props: dict[str, str] 39 | ) -> tuple[list[str], list[str]]: 40 | # pylint: disable=unused-argument 41 | readable_props = [] 42 | writable_props = [] 43 | for _temperature in resource_property: 44 | if _temperature["locationName"] == self._location_name: 45 | attr_name = self._PROFILE["temperature"]["targetTemperature"] 46 | prop = self._get_properties(_temperature, "targetTemperature") 47 | self._set_prop_attr(attr_name, prop) 48 | if prop[READABLE_VALUES]: 49 | readable_props.append(attr_name) 50 | if prop[WRITABLE_VALUES]: 51 | writable_props.append(attr_name) 52 | 53 | return readable_props, writable_props 54 | 55 | 56 | class KimchiRefrigeratorProfile(ConnectDeviceProfile): 57 | def __init__(self, profile: dict[str, Any]): 58 | super().__init__( 59 | profile=profile, 60 | location_map={ 61 | "TOP": Location.TOP, 62 | "MIDDLE": Location.MIDDLE, 63 | "BOTTOM": Location.BOTTOM, 64 | "LEFT": Location.LEFT, 65 | "RIGHT": Location.RIGHT, 66 | "SINGLE": Location.SINGLE, 67 | }, 68 | resource_map={"refrigeration": Resource.REFRIGERATION}, 69 | profile_map={ 70 | "refrigeration": { 71 | "oneTouchFilter": Property.ONE_TOUCH_FILTER, 72 | "freshAirFilter": Property.FRESH_AIR_FILTER, 73 | }, 74 | }, 75 | ) 76 | for temperature_property in profile.get("property", {}).get("temperature", []): 77 | location_name = temperature_property.get("locationName") 78 | if location_name in self._LOCATION_MAP.keys(): 79 | attr_key = self._LOCATION_MAP[location_name] 80 | _sub_profile = KimchiRefrigeratorSubProfile(profile, location_name) 81 | self._set_sub_profile(attr_key, _sub_profile) 82 | self._set_location_properties(attr_key, _sub_profile.properties) 83 | 84 | 85 | class KimchiRefrigeratorSubDevice(ConnectSubDevice): 86 | """KimchiRefrigerator Device Sub.""" 87 | 88 | def __init__( 89 | self, 90 | profiles: KimchiRefrigeratorSubProfile, 91 | location_name: Location, 92 | thinq_api: ThinQApi, 93 | device_id: str, 94 | device_type: str, 95 | model_name: str, 96 | alias: str, 97 | reportable: bool, 98 | ): 99 | super().__init__( 100 | profiles, 101 | location_name, 102 | thinq_api, 103 | device_id, 104 | device_type, 105 | model_name, 106 | alias, 107 | reportable, 108 | is_single_resource=True, 109 | ) 110 | 111 | @property 112 | def profiles(self) -> KimchiRefrigeratorSubProfile: 113 | return self._profiles 114 | 115 | 116 | class KimchiRefrigeratorDevice(ConnectMainDevice): 117 | """KimchiRefrigerator Property.""" 118 | 119 | def __init__( 120 | self, 121 | thinq_api: ThinQApi, 122 | device_id: str, 123 | device_type: str, 124 | model_name: str, 125 | alias: str, 126 | reportable: bool, 127 | profile: dict[str, Any], 128 | ): 129 | self._sub_devices: dict[str, KimchiRefrigeratorSubDevice] = {} 130 | super().__init__( 131 | thinq_api=thinq_api, 132 | device_id=device_id, 133 | device_type=device_type, 134 | model_name=model_name, 135 | alias=alias, 136 | reportable=reportable, 137 | profiles=KimchiRefrigeratorProfile(profile=profile), 138 | sub_device_type=KimchiRefrigeratorSubDevice, 139 | ) 140 | 141 | @property 142 | def profiles(self) -> KimchiRefrigeratorProfile: 143 | return self._profiles 144 | 145 | def get_sub_device(self, location_name: Location) -> KimchiRefrigeratorSubDevice: 146 | return super().get_sub_device(location_name) 147 | -------------------------------------------------------------------------------- /thinqconnect/devices/microwave_oven.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """ 4 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 5 | * SPDX-License-Identifier: Apache-2.0 6 | """ 7 | from typing import Any 8 | 9 | from ..thinq_api import ThinQApi 10 | from .connect_device import ConnectBaseDevice, ConnectDeviceProfile 11 | from .const import Property, Resource 12 | 13 | 14 | class MicrowaveOvenProfile(ConnectDeviceProfile): 15 | def __init__(self, profile: dict[str, Any]): 16 | super().__init__( 17 | profile=profile, 18 | resource_map={ 19 | "runState": Resource.RUN_STATE, 20 | "timer": Resource.TIMER, 21 | "ventilation": Resource.VENTILATION, 22 | "lamp": Resource.LAMP, 23 | }, 24 | profile_map={ 25 | "runState": {"currentState": Property.CURRENT_STATE}, 26 | "timer": { 27 | "remainMinute": Property.REMAIN_MINUTE, 28 | "remainSecond": Property.REMAIN_SECOND, 29 | }, 30 | "ventilation": {"fanSpeed": Property.FAN_SPEED}, 31 | "lamp": {"lampBrightness": Property.LAMP_BRIGHTNESS}, 32 | }, 33 | ) 34 | 35 | 36 | class MicrowaveOvenDevice(ConnectBaseDevice): 37 | def __init__( 38 | self, 39 | thinq_api: ThinQApi, 40 | device_id: str, 41 | device_type: str, 42 | model_name: str, 43 | alias: str, 44 | reportable: bool, 45 | profile: dict[str, Any], 46 | ): 47 | super().__init__( 48 | thinq_api=thinq_api, 49 | device_id=device_id, 50 | device_type=device_type, 51 | model_name=model_name, 52 | alias=alias, 53 | reportable=reportable, 54 | profiles=MicrowaveOvenProfile(profile=profile), 55 | ) 56 | 57 | @property 58 | def profiles(self) -> MicrowaveOvenProfile: 59 | return self._profiles 60 | 61 | async def set_fan_speed_lamp_brightness(self, fan_speed: int, lamp_brightness: int) -> dict | None: 62 | return await self.do_multi_range_attribute_command( 63 | { 64 | Property.FAN_SPEED: fan_speed, 65 | Property.LAMP_BRIGHTNESS: lamp_brightness, 66 | } 67 | ) 68 | 69 | async def set_fan_speed(self, speed: int) -> dict | None: 70 | return await self.do_multi_range_attribute_command( 71 | { 72 | Property.LAMP_BRIGHTNESS: self.get_status(Property.LAMP_BRIGHTNESS), 73 | Property.FAN_SPEED: speed, 74 | } 75 | ) 76 | 77 | async def set_lamp_brightness(self, brightness: int) -> dict | None: 78 | return await self.do_multi_range_attribute_command( 79 | { 80 | Property.LAMP_BRIGHTNESS: brightness, 81 | Property.FAN_SPEED: self.get_status(Property.FAN_SPEED), 82 | } 83 | ) 84 | -------------------------------------------------------------------------------- /thinqconnect/devices/oven.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """ 4 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 5 | * SPDX-License-Identifier: Apache-2.0 6 | """ 7 | from typing import Any 8 | 9 | from ..const import PROPERTY_READABLE 10 | from ..thinq_api import ThinQApi 11 | from .connect_device import ( 12 | READABILITY, 13 | WRITABILITY, 14 | ConnectDeviceProfile, 15 | ConnectMainDevice, 16 | ConnectSubDevice, 17 | ConnectSubDeviceProfile, 18 | ) 19 | from .const import Location, Property, Resource 20 | 21 | 22 | class OvenProfile(ConnectDeviceProfile): 23 | def __init__(self, profile: dict[str, Any]): 24 | super().__init__( 25 | profile=profile, 26 | location_map={ 27 | "OVEN": Location.OVEN, 28 | "UPPER": Location.UPPER, 29 | "LOWER": Location.LOWER, 30 | }, 31 | resource_map={"info": Resource.INFO}, 32 | profile_map={"info": {"type": Property.OVEN_TYPE}}, 33 | use_extension_property=True, 34 | ) 35 | for profile_property in profile.get("property", []): 36 | location_name = profile_property.get("location", {}).get("locationName") 37 | if location_name in self._LOCATION_MAP.keys(): 38 | attr_key = self._LOCATION_MAP[location_name] 39 | _sub_profile = OvenSubProfile(profile, location_name) 40 | self._set_sub_profile(attr_key, _sub_profile) 41 | self._set_location_properties(attr_key, _sub_profile.properties) 42 | 43 | 44 | class OvenSubProfile(ConnectSubDeviceProfile): 45 | _CUSTOM_RESOURCES = ["temperature"] 46 | 47 | def __init__(self, profile: dict[str, Any], location_name: Location = None): 48 | super().__init__( 49 | profile=profile, 50 | location_name=location_name, 51 | resource_map={ 52 | "runState": Resource.RUN_STATE, 53 | "operation": Resource.OPERATION, 54 | "cook": Resource.COOK, 55 | "remoteControlEnable": Resource.REMOTE_CONTROL_ENABLE, 56 | "temperature": Resource.TEMPERATURE, 57 | "timer": Resource.TIMER, 58 | }, 59 | profile_map={ 60 | "runState": {"currentState": Property.CURRENT_STATE}, 61 | "operation": {"ovenOperationMode": Property.OVEN_OPERATION_MODE}, 62 | "cook": {"cookMode": Property.COOK_MODE}, 63 | "remoteControlEnable": {"remoteControlEnabled": Property.REMOTE_CONTROL_ENABLED}, 64 | "temperature": { 65 | "C": Property.TARGET_TEMPERATURE_C, 66 | "F": Property.TARGET_TEMPERATURE_F, 67 | "unit": Property.TEMPERATURE_UNIT, 68 | }, 69 | "timer": { 70 | "remainHour": Property.REMAIN_HOUR, 71 | "remainMinute": Property.REMAIN_MINUTE, 72 | "remainSecond": Property.REMAIN_SECOND, 73 | "targetHour": Property.TARGET_HOUR, 74 | "targetMinute": Property.TARGET_MINUTE, 75 | "targetSecond": Property.TARGET_SECOND, 76 | "timerHour": Property.TIMER_HOUR, 77 | "timerMinute": Property.TIMER_MINUTE, 78 | "timerSecond": Property.TIMER_SECOND, 79 | }, 80 | }, 81 | custom_resources=["temperature"], 82 | ) 83 | 84 | def get_range_attribute_payload(self, attribute: str, value: int) -> dict: 85 | temperature_info = self._get_prop_attr(attribute) 86 | if not self.check_range_attribute_writable(attribute, value): 87 | raise ValueError(f"Not support {attribute}") 88 | return { 89 | "location": {"locationName": self._location_name}, 90 | "temperature": { 91 | "targetTemperature": value, 92 | "unit": temperature_info["unit"], 93 | }, 94 | } 95 | 96 | def _generate_custom_resource_properties( 97 | self, resource_key: str, resource_property: dict | list, props: dict[str, str] 98 | ) -> tuple[list[str], list[str]]: 99 | # pylint: disable=unused-argument 100 | readable_props = [] 101 | writable_props = [] 102 | if resource_key != "temperature": 103 | return readable_props, writable_props 104 | 105 | temperature_map = self._PROFILE[resource_key] 106 | units = [] 107 | for _temperature in resource_property: 108 | _temperature_unit = _temperature["unit"] 109 | if _temperature_unit in temperature_map: 110 | attr_name = temperature_map[_temperature_unit] 111 | prop = self._get_properties(_temperature, "targetTemperature") 112 | self._set_prop_attr(attr_name, prop) 113 | if prop[READABILITY]: 114 | readable_props.append(attr_name) 115 | if prop[WRITABILITY]: 116 | writable_props.append(attr_name) 117 | units.append(_temperature_unit) 118 | 119 | prop_attr = props.get("unit") 120 | prop = self._get_readonly_enum_property(units) 121 | if prop[READABILITY]: 122 | readable_props.append(str(prop_attr)) 123 | if prop[WRITABILITY]: 124 | writable_props.append(str(prop_attr)) 125 | self._set_prop_attr(prop_attr, prop) 126 | 127 | return readable_props, writable_props 128 | 129 | def generate_properties(self, property: dict[str, Any]) -> None: 130 | """Generate properties.""" 131 | for location_property in property: 132 | if location_property.get("location", {}).get("locationName") != self._location_name: 133 | continue 134 | super().generate_properties(location_property) 135 | 136 | 137 | class OvenSubDevice(ConnectSubDevice): 138 | """Oven Device Sub.""" 139 | 140 | _CUSTOM_SET_PROPERTY_NAME = { 141 | Property.TARGET_HOUR: "target_time", 142 | Property.TARGET_MINUTE: "target_time", 143 | Property.TIMER_HOUR: "timer", 144 | Property.TIMER_MINUTE: "timer", 145 | } 146 | 147 | def __init__( 148 | self, 149 | profiles: OvenSubProfile, 150 | location_name: Location, 151 | thinq_api: ThinQApi, 152 | device_id: str, 153 | device_type: str, 154 | model_name: str, 155 | alias: str, 156 | reportable: bool, 157 | ): 158 | super().__init__(profiles, location_name, thinq_api, device_id, device_type, model_name, alias, reportable) 159 | self._temp_unit = None 160 | 161 | @property 162 | def profiles(self) -> OvenSubProfile: 163 | return self._profiles 164 | 165 | @property 166 | def remain_time(self) -> dict: 167 | return { 168 | "hour": self.get_status("remain_hour"), 169 | "minute": self.get_status("remain_minute"), 170 | "second": self.get_status("remain_second"), 171 | } 172 | 173 | @property 174 | def target_time(self) -> dict: 175 | return { 176 | "hour": self.get_status("target_hour"), 177 | "minute": self.get_status("target_minute"), 178 | "second": self.get_status("target_second"), 179 | } 180 | 181 | @property 182 | def timer_time(self) -> dict: 183 | return { 184 | "hour": self.get_status("timer_hour"), 185 | "minute": self.get_status("timer_minute"), 186 | "second": self.get_status("timer_second"), 187 | } 188 | 189 | def _set_custom_resources( 190 | self, 191 | prop_key: str, 192 | attribute: str, 193 | resource_status: dict[str, str] | list[dict[str, str]], 194 | is_updated: bool = False, 195 | ) -> bool: 196 | if attribute is Property.TEMPERATURE_UNIT: 197 | return True 198 | 199 | temperature_map = self.profiles._PROFILE["temperature"] 200 | _temp_status_value = resource_status.get("targetTemperature") 201 | _temp_status_unit = resource_status.get("unit") 202 | 203 | if not _temp_status_unit: 204 | self._set_status_attr( 205 | temperature_map.get(self._temp_unit), 206 | _temp_status_value, 207 | ) 208 | return True 209 | 210 | _temp_attr_name = temperature_map.get(_temp_status_unit) 211 | if attribute == _temp_attr_name: 212 | self._temp_unit = _temp_status_unit 213 | self._set_status_attr(Property.TEMPERATURE_UNIT, self._temp_unit) 214 | _attribute_value = _temp_status_value 215 | elif is_updated: 216 | return True 217 | else: 218 | _attribute_value = None 219 | 220 | self._set_status_attr(attribute, _attribute_value) 221 | return True 222 | 223 | async def set_oven_operation_mode(self, mode: str) -> dict | None: 224 | payload = self.profiles.get_enum_attribute_payload("oven_operation_mode", mode) 225 | return await self._do_attribute_command({"location": {"locationName": self._location_name}, **payload}) 226 | 227 | async def set_cook_mode(self, mode: str) -> dict | None: 228 | payload = self.profiles.get_enum_attribute_payload("cook_mode", mode) 229 | return await self._do_attribute_command({"location": {"locationName": self._location_name}, **payload}) 230 | 231 | async def _set_target_temperature(self, target_temperature: int | float, unit: str) -> dict | None: 232 | temperature_map = self.profiles._PROFILE["temperature"] 233 | payload = self.profiles.get_range_attribute_payload(temperature_map.get(unit), target_temperature) 234 | return await self._do_attribute_command({"location": {"locationName": self._location_name}, **payload}) 235 | 236 | async def set_target_temperature_f(self, target_temperature: int | float) -> dict | None: 237 | return await self._set_target_temperature(target_temperature, "F") 238 | 239 | async def set_target_temperature_c(self, target_temperature: int | float) -> dict | None: 240 | return await self._set_target_temperature(target_temperature, "C") 241 | 242 | async def set_target_time(self, target_hour: int, target_minute: int) -> dict | None: 243 | payload = self.profiles.get_enum_attribute_payload("oven_operation_mode", "START") 244 | target_time_payload = self.profiles.get_attribute_payload(Property.TARGET_HOUR, target_hour) 245 | target_minute_payload = self.profiles.get_attribute_payload(Property.TARGET_MINUTE, target_minute) 246 | for key, sub_dict in target_minute_payload.items(): 247 | target_time_payload[key].update(sub_dict) 248 | return await self._do_attribute_command( 249 | {"location": {"locationName": self._location_name}, **payload, **target_time_payload} 250 | ) 251 | 252 | async def set_timer(self, hour: int, minute: int) -> dict | None: 253 | return await self.do_multi_attribute_command( 254 | { 255 | Property.TIMER_HOUR: hour, 256 | Property.TIMER_MINUTE: minute, 257 | } 258 | ) 259 | 260 | 261 | class OvenDevice(ConnectMainDevice): 262 | """Oven Property.""" 263 | 264 | def __init__( 265 | self, 266 | thinq_api: ThinQApi, 267 | device_id: str, 268 | device_type: str, 269 | model_name: str, 270 | alias: str, 271 | reportable: bool, 272 | profile: dict[str, Any], 273 | ): 274 | self._sub_devices: dict[str, OvenSubDevice] = {} 275 | super().__init__( 276 | thinq_api=thinq_api, 277 | device_id=device_id, 278 | device_type=device_type, 279 | model_name=model_name, 280 | alias=alias, 281 | reportable=reportable, 282 | profiles=OvenProfile(profile=profile), 283 | sub_device_type=OvenSubDevice, 284 | ) 285 | self.oven_type = self.profiles.get_property("oven_type").get(PROPERTY_READABLE, [None])[0] 286 | 287 | @property 288 | def profiles(self) -> OvenProfile: 289 | return self._profiles 290 | 291 | def get_sub_device(self, location_name: Location) -> OvenSubDevice: 292 | return super().get_sub_device(location_name) 293 | -------------------------------------------------------------------------------- /thinqconnect/devices/plant_cultivator.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """ 4 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 5 | * SPDX-License-Identifier: Apache-2.0 6 | """ 7 | from typing import Any 8 | 9 | from ..thinq_api import ThinQApi 10 | from .connect_device import ConnectDeviceProfile, ConnectMainDevice, ConnectSubDevice 11 | from .const import Location, Property, Resource 12 | 13 | 14 | class PlantCultivatorProfile(ConnectDeviceProfile): 15 | def __init__(self, profile: dict[str, Any]): 16 | super().__init__( 17 | profile=profile, 18 | location_map={ 19 | "UPPER": Location.UPPER, 20 | "LOWER": Location.LOWER, 21 | }, 22 | use_sub_profile_only=True, 23 | ) 24 | for profile_property in profile.get("property", []): 25 | location_name = profile_property.get("location", {}).get("locationName") 26 | if location_name in self._LOCATION_MAP.keys(): 27 | attr_key = self._LOCATION_MAP[location_name] 28 | _sub_profile = PlantCultivatorSubProfile(profile, location_name) 29 | self._set_sub_profile(attr_key, _sub_profile) 30 | self._set_location_properties(attr_key, _sub_profile.properties) 31 | self.generate_property_map() 32 | 33 | 34 | class PlantCultivatorSubProfile(ConnectDeviceProfile): 35 | def __init__(self, profile: dict[str, Any], location_name: Location): 36 | self._location_name = location_name 37 | super().__init__( 38 | profile=profile, 39 | resource_map={ 40 | "runState": Resource.RUN_STATE, 41 | "light": Resource.LIGHT, 42 | "temperature": Resource.TEMPERATURE, 43 | }, 44 | profile_map={ 45 | "runState": { 46 | "currentState": Property.CURRENT_STATE, 47 | "growthMode": Property.GROWTH_MODE, 48 | "windVolume": Property.WIND_VOLUME, 49 | }, 50 | "light": { 51 | "brightness": Property.BRIGHTNESS, 52 | "duration": Property.DURATION, 53 | "startHour": Property.START_HOUR, 54 | "startMinute": Property.START_MINUTE, 55 | }, 56 | "temperature": { 57 | "dayTargetTemperature": Property.DAY_TARGET_TEMPERATURE, 58 | "nightTargetTemperature": Property.NIGHT_TARGET_TEMPERATURE, 59 | "temperatureState": Property.TEMPERATURE_STATE, 60 | }, 61 | }, 62 | ) 63 | 64 | def generate_properties(self, property: dict[str, Any]) -> None: 65 | """Get properties.""" 66 | for location_property in property: 67 | if location_property.get("location", {}).get("locationName") != self._location_name: 68 | continue 69 | super().generate_properties(location_property) 70 | 71 | 72 | class PlantCultivatorSubDevice(ConnectSubDevice): 73 | def __init__( 74 | self, 75 | profiles: PlantCultivatorSubProfile, 76 | location_name: Location, 77 | thinq_api: ThinQApi, 78 | device_id: str, 79 | device_type: str, 80 | model_name: str, 81 | alias: str, 82 | reportable: bool, 83 | ): 84 | super().__init__(profiles, location_name, thinq_api, device_id, device_type, model_name, alias, reportable) 85 | 86 | @property 87 | def profiles(self) -> PlantCultivatorSubProfile: 88 | return self._profiles 89 | 90 | 91 | class PlantCultivatorDevice(ConnectMainDevice): 92 | """PlantCultivator Property.""" 93 | 94 | def __init__( 95 | self, 96 | thinq_api: ThinQApi, 97 | device_id: str, 98 | device_type: str, 99 | model_name: str, 100 | alias: str, 101 | reportable: bool, 102 | profile: dict[str, Any], 103 | ): 104 | self._sub_devices: dict[str, PlantCultivatorSubDevice] = {} 105 | super().__init__( 106 | thinq_api=thinq_api, 107 | device_id=device_id, 108 | device_type=device_type, 109 | model_name=model_name, 110 | alias=alias, 111 | reportable=reportable, 112 | profiles=PlantCultivatorProfile(profile=profile), 113 | sub_device_type=PlantCultivatorSubDevice, 114 | ) 115 | 116 | @property 117 | def profiles(self) -> PlantCultivatorProfile: 118 | return self._profiles 119 | 120 | def get_sub_device(self, location_name: Location) -> PlantCultivatorSubDevice: 121 | return super().get_sub_device(location_name) 122 | -------------------------------------------------------------------------------- /thinqconnect/devices/refrigerator.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """ 4 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 5 | * SPDX-License-Identifier: Apache-2.0 6 | """ 7 | from typing import Any 8 | 9 | from ..thinq_api import ThinQApi 10 | from .connect_device import ( 11 | READABILITY, 12 | WRITABILITY, 13 | ConnectDeviceProfile, 14 | ConnectMainDevice, 15 | ConnectSubDevice, 16 | ConnectSubDeviceProfile, 17 | ) 18 | from .const import Location, Property, Resource 19 | 20 | 21 | class RefrigeratorSubProfile(ConnectSubDeviceProfile): 22 | def __init__(self, profile: dict[str, Any], location_name: Location): 23 | super().__init__( 24 | profile=profile, 25 | location_name=location_name, 26 | resource_map={ 27 | "doorStatus": Resource.DOOR_STATUS, 28 | "temperatureInUnits": Resource.TEMPERATURE, 29 | }, 30 | profile_map={ 31 | "doorStatus": { 32 | "doorState": Property.DOOR_STATE, 33 | }, 34 | "temperatureInUnits": { 35 | "targetTemperatureC": Property.TARGET_TEMPERATURE_C, 36 | "targetTemperatureF": Property.TARGET_TEMPERATURE_F, 37 | "unit": Property.TEMPERATURE_UNIT, 38 | }, 39 | }, 40 | custom_resources=["doorStatus", "temperatureInUnits"], 41 | ) 42 | 43 | def _generate_custom_resource_properties( 44 | self, resource_key: str, resource_property: dict | list, props: dict[str, str] 45 | ) -> tuple[list[str], list[str]]: 46 | # pylint: disable=unused-argument 47 | readable_props = [] 48 | writable_props = [] 49 | if resource_key not in self._PROFILE.keys(): 50 | return readable_props, writable_props 51 | 52 | for _location_property in resource_property: 53 | if _location_property["locationName"] != self._location_name: 54 | continue 55 | for prop_key, prop_attr in props.items(): 56 | prop = self._get_properties(_location_property, prop_key) 57 | prop.pop("unit", None) 58 | if prop[READABILITY]: 59 | readable_props.append(str(prop_attr)) 60 | if prop[WRITABILITY]: 61 | writable_props.append(str(prop_attr)) 62 | self._set_prop_attr(prop_attr, prop) 63 | 64 | return readable_props, writable_props 65 | 66 | 67 | class RefrigeratorProfile(ConnectDeviceProfile): 68 | _DOOR_LOCATION_MAP = {"MAIN": Location.MAIN} 69 | _TEMPERATURE_LOCATION_MAP = { 70 | "FRIDGE": Location.FRIDGE, 71 | "FREEZER": Location.FREEZER, 72 | "CONVERTIBLE": Location.CONVERTIBLE, 73 | } 74 | 75 | def __init__(self, profile: dict[str, Any]): 76 | super().__init__( 77 | profile, 78 | resource_map={ 79 | "powerSave": Resource.POWER_SAVE, 80 | "ecoFriendly": Resource.ECO_FRIENDLY, 81 | "sabbath": Resource.SABBATH, 82 | "refrigeration": Resource.REFRIGERATION, 83 | "waterFilterInfo": Resource.WATER_FILTER_INFO, 84 | }, 85 | profile_map={ 86 | "powerSave": { 87 | "powerSaveEnabled": Property.POWER_SAVE_ENABLED, 88 | }, 89 | "ecoFriendly": { 90 | "ecoFriendlyMode": Property.ECO_FRIENDLY_MODE, 91 | }, 92 | "sabbath": { 93 | "sabbathMode": Property.SABBATH_MODE, 94 | }, 95 | "refrigeration": { 96 | "rapidFreeze": Property.RAPID_FREEZE, 97 | "expressMode": Property.EXPRESS_MODE, 98 | "expressModeName": Property.EXPRESS_MODE_NAME, 99 | "expressFridge": Property.EXPRESS_FRIDGE, 100 | "freshAirFilter": Property.FRESH_AIR_FILTER, 101 | }, 102 | "waterFilterInfo": { 103 | "usedTime": Property.USED_TIME, 104 | "unit": Property.WATER_FILTER_INFO_UNIT, 105 | }, 106 | }, 107 | ) 108 | 109 | for location_property in profile.get("property", {}).get("doorStatus", []): 110 | location_name = location_property.get("locationName") 111 | if location_name in self._DOOR_LOCATION_MAP.keys(): 112 | attr_key = self._DOOR_LOCATION_MAP[location_name] 113 | _sub_profile = RefrigeratorSubProfile(profile, location_name) 114 | self._set_sub_profile(attr_key, _sub_profile) 115 | self._set_location_properties(attr_key, _sub_profile.properties) 116 | 117 | for location_property in profile.get("property", {}).get("temperatureInUnits", []): 118 | location_name = location_property.get("locationName") 119 | if location_name in self._TEMPERATURE_LOCATION_MAP.keys(): 120 | attr_key = self._TEMPERATURE_LOCATION_MAP[location_name] 121 | _sub_profile = RefrigeratorSubProfile(profile, location_name) 122 | self._set_sub_profile(attr_key, _sub_profile) 123 | self._set_location_properties(attr_key, _sub_profile.properties) 124 | 125 | def get_location_key(self, location_name: Location) -> str | None: 126 | for key, name in self._DOOR_LOCATION_MAP.items(): 127 | if name == location_name: 128 | return key 129 | for key, name in self._TEMPERATURE_LOCATION_MAP.items(): 130 | if name == location_name: 131 | return key 132 | 133 | 134 | class RefrigeratorSubDevice(ConnectSubDevice): 135 | """Refrigerator Device Sub.""" 136 | 137 | def __init__( 138 | self, 139 | profiles: RefrigeratorSubProfile, 140 | location_name: Location, 141 | thinq_api: ThinQApi, 142 | device_id: str, 143 | device_type: str, 144 | model_name: str, 145 | alias: str, 146 | reportable: bool, 147 | ): 148 | super().__init__( 149 | profiles, 150 | location_name, 151 | thinq_api, 152 | device_id, 153 | device_type, 154 | model_name, 155 | alias, 156 | reportable, 157 | is_single_resource=True, 158 | ) 159 | 160 | @property 161 | def profiles(self) -> RefrigeratorSubProfile: 162 | return self._profiles 163 | 164 | def _set_custom_resources( 165 | self, 166 | prop_key: str, 167 | attribute: str, 168 | resource_status: dict[str, str] | list[dict[str, str]], 169 | is_updated: bool = False, 170 | ) -> bool: 171 | if is_updated and attribute in [Property.TARGET_TEMPERATURE_C, Property.TARGET_TEMPERATURE_F]: 172 | current_unit = resource_status.get("unit") or self.get_status(Property.TEMPERATURE_UNIT) 173 | if attribute[-1:].upper() == current_unit: 174 | self._set_status_attr(attribute, value=resource_status.get(prop_key)) 175 | return True 176 | return False 177 | 178 | async def _set_target_temperature(self, temperature: int | float, unit: str) -> dict | None: 179 | _resource_key = "temperatureInUnits" 180 | _target_temperature_key = self.get_property_key(_resource_key, "targetTemperature" + unit) 181 | 182 | _payload = self.profiles.get_range_attribute_payload(_target_temperature_key, temperature) 183 | _payload[_resource_key] = dict( 184 | { 185 | "locationName": self._location_name, 186 | }, 187 | **(_payload[_resource_key]), 188 | ) 189 | return await self._do_attribute_command(_payload) 190 | 191 | async def set_target_temperature_c(self, temperature: int | float) -> dict | None: 192 | return await self._set_target_temperature(temperature, "C") 193 | 194 | async def set_target_temperature_f(self, temperature: int | float) -> dict | None: 195 | return await self._set_target_temperature(temperature, "F") 196 | 197 | 198 | class RefrigeratorDevice(ConnectMainDevice): 199 | """Refrigerator Property.""" 200 | 201 | def __init__( 202 | self, 203 | thinq_api: ThinQApi, 204 | device_id: str, 205 | device_type: str, 206 | model_name: str, 207 | alias: str, 208 | reportable: bool, 209 | profile: dict[str, Any], 210 | ): 211 | self._sub_devices: dict[str, RefrigeratorSubDevice] = {} 212 | super().__init__( 213 | thinq_api=thinq_api, 214 | device_id=device_id, 215 | device_type=device_type, 216 | model_name=model_name, 217 | alias=alias, 218 | reportable=reportable, 219 | profiles=RefrigeratorProfile(profile=profile), 220 | sub_device_type=RefrigeratorSubDevice, 221 | ) 222 | 223 | @property 224 | def profiles(self) -> RefrigeratorProfile: 225 | return self._profiles 226 | 227 | def get_sub_device(self, location_name: Location) -> RefrigeratorSubDevice: 228 | return super().get_sub_device(location_name) 229 | 230 | async def set_rapid_freeze(self, mode: bool) -> dict | None: 231 | return await self.do_attribute_command(Property.RAPID_FREEZE, mode) 232 | 233 | async def set_express_mode(self, mode: bool) -> dict | None: 234 | return await self.do_attribute_command(Property.EXPRESS_MODE, mode) 235 | 236 | async def set_express_fridge(self, mode: bool) -> dict | None: 237 | return await self.do_attribute_command(Property.EXPRESS_FRIDGE, mode) 238 | 239 | async def set_fresh_air_filter(self, mode: str) -> dict | None: 240 | return await self.do_enum_attribute_command(Property.FRESH_AIR_FILTER, mode) 241 | -------------------------------------------------------------------------------- /thinqconnect/devices/robot_cleaner.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """ 4 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 5 | * SPDX-License-Identifier: Apache-2.0 6 | """ 7 | from typing import Any 8 | 9 | from ..thinq_api import ThinQApi 10 | from .connect_device import ConnectBaseDevice, ConnectDeviceProfile 11 | from .const import Property, Resource 12 | 13 | 14 | class RobotCleanerProfile(ConnectDeviceProfile): 15 | def __init__(self, profile: dict[str, Any]): 16 | super().__init__( 17 | profile=profile, 18 | resource_map={ 19 | "runState": Resource.RUN_STATE, 20 | "robotCleanerJobMode": Resource.ROBOT_CLEANER_JOB_MODE, 21 | "operation": Resource.OPERATION, 22 | "battery": Resource.BATTERY, 23 | "timer": Resource.TIMER, 24 | }, 25 | profile_map={ 26 | "runState": {"currentState": Property.CURRENT_STATE}, 27 | "robotCleanerJobMode": {"currentJobMode": Property.CURRENT_JOB_MODE}, 28 | "operation": {"cleanOperationMode": Property.CLEAN_OPERATION_MODE}, 29 | "battery": {"level": Property.BATTERY_LEVEL, "percent": Property.BATTERY_PERCENT}, 30 | "timer": { 31 | "absoluteHourToStart": Property.ABSOLUTE_HOUR_TO_START, 32 | "absoluteMinuteToStart": Property.ABSOLUTE_MINUTE_TO_START, 33 | "runningHour": Property.RUNNING_HOUR, 34 | "runningMinute": Property.RUNNING_MINUTE, 35 | }, 36 | }, 37 | ) 38 | 39 | 40 | class RobotCleanerDevice(ConnectBaseDevice): 41 | """RobotCleaner Property.""" 42 | 43 | _CUSTOM_SET_PROPERTY_NAME = { 44 | Property.ABSOLUTE_HOUR_TO_START: "absolute_time_to_start", 45 | Property.ABSOLUTE_MINUTE_TO_START: "absolute_time_to_start", 46 | } 47 | 48 | def __init__( 49 | self, 50 | thinq_api: ThinQApi, 51 | device_id: str, 52 | device_type: str, 53 | model_name: str, 54 | alias: str, 55 | reportable: bool, 56 | profile: dict[str, Any], 57 | ): 58 | super().__init__( 59 | thinq_api=thinq_api, 60 | device_id=device_id, 61 | device_type=device_type, 62 | model_name=model_name, 63 | alias=alias, 64 | reportable=reportable, 65 | profiles=RobotCleanerProfile(profile=profile), 66 | ) 67 | 68 | @property 69 | def profiles(self) -> RobotCleanerProfile: 70 | return self._profiles 71 | 72 | async def set_clean_operation_mode(self, mode: str) -> dict | None: 73 | return await self.do_enum_attribute_command(Property.CLEAN_OPERATION_MODE, mode) 74 | 75 | async def set_absolute_time_to_start(self, hour: int, minute: int) -> dict | None: 76 | return await self.do_multi_attribute_command( 77 | { 78 | Property.ABSOLUTE_HOUR_TO_START: hour, 79 | **({Property.ABSOLUTE_MINUTE_TO_START: minute} if minute != 0 else {}), 80 | } 81 | ) 82 | -------------------------------------------------------------------------------- /thinqconnect/devices/stick_cleaner.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """ 4 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 5 | * SPDX-License-Identifier: Apache-2.0 6 | """ 7 | from typing import Any 8 | 9 | from ..thinq_api import ThinQApi 10 | from .connect_device import ConnectBaseDevice, ConnectDeviceProfile 11 | from .const import Property, Resource 12 | 13 | 14 | class StickCleanerProfile(ConnectDeviceProfile): 15 | def __init__(self, profile: dict[str, Any]): 16 | super().__init__( 17 | profile=profile, 18 | resource_map={ 19 | "runState": Resource.RUN_STATE, 20 | "stickCleanerJobMode": Resource.STICK_CLEANER_JOB_MODE, 21 | "battery": Resource.BATTERY, 22 | }, 23 | profile_map={ 24 | "runState": {"currentState": Property.CURRENT_STATE}, 25 | "stickCleanerJobMode": { 26 | "currentJobMode": Property.CURRENT_JOB_MODE, 27 | }, 28 | "battery": {"level": Property.BATTERY_LEVEL, "percent": Property.BATTERY_PERCENT}, 29 | }, 30 | ) 31 | 32 | 33 | class StickCleanerDevice(ConnectBaseDevice): 34 | """StickCleaner Property.""" 35 | 36 | def __init__( 37 | self, 38 | thinq_api: ThinQApi, 39 | device_id: str, 40 | device_type: str, 41 | model_name: str, 42 | alias: str, 43 | reportable: bool, 44 | profile: dict[str, Any], 45 | ): 46 | super().__init__( 47 | thinq_api=thinq_api, 48 | device_id=device_id, 49 | device_type=device_type, 50 | model_name=model_name, 51 | alias=alias, 52 | reportable=reportable, 53 | profiles=StickCleanerProfile(profile=profile), 54 | ) 55 | 56 | @property 57 | def profiles(self) -> StickCleanerProfile: 58 | return self._profiles 59 | -------------------------------------------------------------------------------- /thinqconnect/devices/styler.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """ 4 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 5 | * SPDX-License-Identifier: Apache-2.0 6 | """ 7 | from typing import Any 8 | 9 | from ..thinq_api import ThinQApi 10 | from .connect_device import ConnectBaseDevice, ConnectDeviceProfile 11 | from .const import Property, Resource 12 | 13 | 14 | class StylerProfile(ConnectDeviceProfile): 15 | def __init__(self, profile: dict[str, Any]): 16 | super().__init__( 17 | profile=profile, 18 | resource_map={ 19 | "runState": Resource.RUN_STATE, 20 | "operation": Resource.OPERATION, 21 | "remoteControlEnable": Resource.REMOTE_CONTROL_ENABLE, 22 | "timer": Resource.TIMER, 23 | }, 24 | profile_map={ 25 | "runState": {"currentState": Property.CURRENT_STATE}, 26 | "operation": {"stylerOperationMode": Property.STYLER_OPERATION_MODE}, 27 | "remoteControlEnable": {"remoteControlEnabled": Property.REMOTE_CONTROL_ENABLED}, 28 | "timer": { 29 | "relativeHourToStop": Property.RELATIVE_HOUR_TO_STOP, 30 | "relativeMinuteToStop": Property.RELATIVE_MINUTE_TO_STOP, 31 | "remainHour": Property.REMAIN_HOUR, 32 | "remainMinute": Property.REMAIN_MINUTE, 33 | "totalHour": Property.TOTAL_HOUR, 34 | "totalMinute": Property.TOTAL_MINUTE, 35 | }, 36 | }, 37 | ) 38 | 39 | 40 | class StylerDevice(ConnectBaseDevice): 41 | """Styler Property.""" 42 | 43 | def __init__( 44 | self, 45 | thinq_api: ThinQApi, 46 | device_id: str, 47 | device_type: str, 48 | model_name: str, 49 | alias: str, 50 | reportable: bool, 51 | profile: dict[str, Any], 52 | ): 53 | super().__init__( 54 | thinq_api=thinq_api, 55 | device_id=device_id, 56 | device_type=device_type, 57 | model_name=model_name, 58 | alias=alias, 59 | reportable=reportable, 60 | profiles=StylerProfile(profile=profile), 61 | ) 62 | 63 | @property 64 | def profiles(self) -> StylerProfile: 65 | return self._profiles 66 | 67 | async def set_styler_operation_mode(self, mode: str) -> dict | None: 68 | return await self.do_enum_attribute_command(Property.STYLER_OPERATION_MODE, mode) 69 | 70 | async def set_relative_hour_to_stop(self, hour: int) -> dict | None: 71 | return await self.do_range_attribute_command(Property.RELATIVE_HOUR_TO_STOP, hour) 72 | -------------------------------------------------------------------------------- /thinqconnect/devices/ventilator.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """ 4 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 5 | * SPDX-License-Identifier: Apache-2.0 6 | """ 7 | from typing import Any 8 | 9 | from ..thinq_api import ThinQApi 10 | from .connect_device import READABILITY, WRITABILITY, ConnectBaseDevice, ConnectDeviceProfile 11 | from .const import Property, Resource 12 | 13 | 14 | class VentilatorProfile(ConnectDeviceProfile): 15 | def __init__(self, profile: dict[str, Any]): 16 | super().__init__( 17 | profile=profile, 18 | resource_map={ 19 | "ventJobMode": Resource.VENTILATOR_JOB_MODE, 20 | "operation": Resource.OPERATION, 21 | "temperature": Resource.TEMPERATURE, 22 | "airQualitySensor": Resource.AIR_QUALITY_SENSOR, 23 | "airFlow": Resource.AIR_FLOW, 24 | "timer": Resource.TIMER, 25 | "sleepTimer": Resource.SLEEP_TIMER, 26 | }, 27 | profile_map={ 28 | "ventJobMode": {"currentJobMode": Property.CURRENT_JOB_MODE}, 29 | "operation": {"ventOperationMode": Property.VENTILATOR_OPERATION_MODE}, 30 | "temperature": { 31 | "currentTemperature": Property.CURRENT_TEMPERATURE, 32 | "unit": Property.TEMPERATURE_UNIT, 33 | }, 34 | "airQualitySensor": { 35 | "PM1": Property.PM1, 36 | "PM2": Property.PM2, 37 | "PM10": Property.PM10, 38 | "CO2": Property.CO2, 39 | }, 40 | "airFlow": {"windStrength": Property.WIND_STRENGTH}, 41 | "timer": { 42 | "absoluteHourToStop": Property.ABSOLUTE_HOUR_TO_STOP, 43 | "absoluteMinuteToStop": Property.ABSOLUTE_MINUTE_TO_STOP, 44 | "absoluteHourToStart": Property.ABSOLUTE_HOUR_TO_START, 45 | "absoluteMinuteToStart": Property.ABSOLUTE_MINUTE_TO_START, 46 | "relativeHourToStop": Property.RELATIVE_HOUR_TO_STOP, 47 | "relativeMinuteToStop": Property.RELATIVE_MINUTE_TO_STOP, 48 | "relativeHourToStart": Property.RELATIVE_HOUR_TO_START, 49 | "relativeMinuteToStart": Property.RELATIVE_MINUTE_TO_START, 50 | }, 51 | "sleepTimer": { 52 | "relativeHourToStop": Property.SLEEP_TIMER_RELATIVE_HOUR_TO_STOP, 53 | "relativeMinuteToStop": Property.SLEEP_TIMER_RELATIVE_MINUTE_TO_STOP, 54 | }, 55 | }, 56 | custom_resources=["temperature"], 57 | ) 58 | 59 | def _generate_custom_resource_properties( 60 | self, resource_key: str, resource_property: dict | list, props: dict[str, str] 61 | ) -> tuple[list[str], list[str]]: 62 | readable_props = [] 63 | writable_props = [] 64 | 65 | if resource_key not in self._CUSTOM_RESOURCES: 66 | return readable_props, writable_props 67 | 68 | for prop_key, prop_attr in props.items(): 69 | prop = self._get_properties(resource_property, prop_key) 70 | prop.pop("unit", None) 71 | if prop[READABILITY]: 72 | readable_props.append(str(prop_attr)) 73 | if prop[WRITABILITY]: 74 | writable_props.append(str(prop_attr)) 75 | self._set_prop_attr(prop_attr, prop) 76 | 77 | return readable_props, writable_props 78 | 79 | 80 | class VentilatorDevice(ConnectBaseDevice): 81 | """Ventilator Property.""" 82 | 83 | _CUSTOM_SET_PROPERTY_NAME = { 84 | Property.RELATIVE_HOUR_TO_START: "relative_time_to_start", 85 | Property.RELATIVE_MINUTE_TO_START: "relative_time_to_start", 86 | Property.RELATIVE_HOUR_TO_STOP: "relative_time_to_stop", 87 | Property.RELATIVE_MINUTE_TO_STOP: "relative_time_to_stop", 88 | Property.ABSOLUTE_HOUR_TO_START: "absolute_time_to_start", 89 | Property.ABSOLUTE_MINUTE_TO_START: "absolute_time_to_start", 90 | Property.ABSOLUTE_HOUR_TO_STOP: "absolute_time_to_stop", 91 | Property.ABSOLUTE_MINUTE_TO_STOP: "absolute_time_to_stop", 92 | Property.SLEEP_TIMER_RELATIVE_HOUR_TO_STOP: "sleep_timer_relative_time_to_stop", 93 | Property.SLEEP_TIMER_RELATIVE_MINUTE_TO_STOP: "sleep_timer_relative_time_to_stop", 94 | } 95 | 96 | def __init__( 97 | self, 98 | thinq_api: ThinQApi, 99 | device_id: str, 100 | device_type: str, 101 | model_name: str, 102 | alias: str, 103 | reportable: bool, 104 | profile: dict[str, Any], 105 | ): 106 | super().__init__( 107 | thinq_api=thinq_api, 108 | device_id=device_id, 109 | device_type=device_type, 110 | model_name=model_name, 111 | alias=alias, 112 | reportable=reportable, 113 | profiles=VentilatorProfile(profile=profile), 114 | ) 115 | 116 | @property 117 | def profiles(self) -> VentilatorProfile: 118 | return self._profiles 119 | 120 | async def set_current_job_mode(self, job_mode: str) -> dict | None: 121 | return await self.do_enum_attribute_command(Property.CURRENT_JOB_MODE, job_mode) 122 | 123 | async def set_ventilator_operation_mode(self, operation_mode: str) -> dict | None: 124 | return await self.do_enum_attribute_command(Property.VENTILATOR_OPERATION_MODE, operation_mode) 125 | 126 | async def set_absolute_time_to_start(self, hour: int, minute: int) -> dict | None: 127 | return await self.do_multi_attribute_command( 128 | { 129 | Property.ABSOLUTE_HOUR_TO_START: hour, 130 | Property.ABSOLUTE_MINUTE_TO_START: minute, 131 | } 132 | ) 133 | 134 | async def set_absolute_time_to_stop(self, hour: int, minute: int) -> dict | None: 135 | return await self.do_multi_attribute_command( 136 | { 137 | Property.ABSOLUTE_HOUR_TO_STOP: hour, 138 | Property.ABSOLUTE_MINUTE_TO_STOP: minute, 139 | } 140 | ) 141 | 142 | async def set_sleep_timer_relative_time_to_stop(self, hour: int, minute: int = 0) -> dict | None: 143 | return await self.do_multi_attribute_command( 144 | { 145 | Property.SLEEP_TIMER_RELATIVE_HOUR_TO_STOP: hour, 146 | **({Property.SLEEP_TIMER_RELATIVE_MINUTE_TO_STOP: minute} if minute != 0 else {}), 147 | } 148 | ) 149 | 150 | async def set_wind_strength(self, wind_strength: str) -> dict | None: 151 | return await self.do_enum_attribute_command(Property.WIND_STRENGTH, wind_strength) 152 | -------------------------------------------------------------------------------- /thinqconnect/devices/washcombo.py: -------------------------------------------------------------------------------- 1 | """ 2 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | """ 5 | from typing import Any 6 | 7 | from ..thinq_api import ThinQApi 8 | from .connect_device import ConnectSubDeviceProfile 9 | from .const import Location, Property, Resource 10 | from .washer import WasherSubDevice 11 | 12 | 13 | class WashcomboProfile(ConnectSubDeviceProfile): 14 | def __init__(self, profile: dict[str, Any], location_name: Location = None, use_sub_notification: bool = False): 15 | super().__init__( 16 | profile, 17 | location_name=location_name, 18 | resource_map={ 19 | "runState": Resource.RUN_STATE, 20 | "operation": Resource.OPERATION, 21 | "mode": Resource.MODE, 22 | "remoteControlEnable": Resource.REMOTE_CONTROL_ENABLE, 23 | "timer": Resource.TIMER, 24 | "detergent": Resource.DETERGENT, 25 | "cycle": Resource.CYCLE, 26 | }, 27 | profile_map={ 28 | "runState": {"currentState": Property.CURRENT_STATE}, 29 | "operation": { 30 | "washerOperationMode": Property.WASHER_OPERATION_MODE, 31 | }, 32 | "mode": { 33 | "washerMode": Property.WASHER_MODE, 34 | }, 35 | "remoteControlEnable": {"remoteControlEnabled": Property.REMOTE_CONTROL_ENABLED}, 36 | "timer": { 37 | "remainHour": Property.REMAIN_HOUR, 38 | "remainMinute": Property.REMAIN_MINUTE, 39 | "totalHour": Property.TOTAL_HOUR, 40 | "totalMinute": Property.TOTAL_MINUTE, 41 | "relativeHourToStop": Property.RELATIVE_HOUR_TO_STOP, 42 | "relativeMinuteToStop": Property.RELATIVE_MINUTE_TO_STOP, 43 | "relativeHourToStart": Property.RELATIVE_HOUR_TO_START, 44 | "relativeMinuteToStart": Property.RELATIVE_MINUTE_TO_START, 45 | }, 46 | "detergent": {"detergentSetting": Property.DETERGENT_SETTING}, 47 | "cycle": {"cycleCount": Property.CYCLE_COUNT}, 48 | }, 49 | use_sub_notification=use_sub_notification, 50 | ) 51 | 52 | def generate_properties(self, property: list[dict[str, Any]] | dict[str, Any]) -> None: 53 | """Get properties.""" 54 | if isinstance(property, list): 55 | for location_property in property: 56 | if location_property.get("location", {}).get("locationName") != self._location_name: 57 | continue 58 | super().generate_properties(location_property) 59 | else: 60 | super().generate_properties(property) 61 | 62 | 63 | class WashcomboDevice(WasherSubDevice): 64 | @property 65 | def location(self) -> str: 66 | return self._location 67 | 68 | @location.setter 69 | def location(self, value: str): 70 | self._location = value 71 | 72 | @property 73 | def group_id(self) -> str: 74 | return self._group_id 75 | 76 | @group_id.setter 77 | def group_id(self, value: str): 78 | self._group_id = value 79 | 80 | async def set_washer_operation_mode(self, operation: str) -> dict | None: 81 | payload = self.profiles.get_enum_attribute_payload(Property.WASHER_OPERATION_MODE, operation) 82 | return await self._do_attribute_command({"location": {"locationName": self.location}, **payload}) 83 | 84 | async def set_washer_mode(self, mode: str) -> dict | None: 85 | operation_payload = self.profiles.get_enum_attribute_payload(Property.WASHER_OPERATION_MODE, "START") 86 | payload = self.profiles.get_enum_attribute_payload(Property.WASHER_MODE, mode) 87 | return await self._do_attribute_command( 88 | {"location": {"locationName": self.location}, **operation_payload, **payload} 89 | ) 90 | 91 | async def set_relative_hour_to_start(self, hour: int) -> dict | None: 92 | payload = self.profiles.get_range_attribute_payload(Property.RELATIVE_HOUR_TO_START, hour) 93 | return await self._do_attribute_command({"location": {"locationName": self.location}, **payload}) 94 | 95 | async def set_relative_hour_to_stop(self, hour: int) -> dict | None: 96 | payload = self.profiles.get_range_attribute_payload(Property.RELATIVE_HOUR_TO_STOP, hour) 97 | return await self._do_attribute_command({"location": {"locationName": self.location}, **payload}) 98 | 99 | def __init__( 100 | self, 101 | thinq_api: ThinQApi, 102 | device_id: str, 103 | device_type: str, 104 | model_name: str, 105 | alias: str, 106 | group_id: str, 107 | reportable: bool, 108 | profile: dict[str, Any], 109 | location: str, 110 | ): 111 | super().__init__( 112 | profiles=WashcomboProfile(profile=profile, location_name=location, use_sub_notification=True), 113 | location_name=location, 114 | single_unit=True, 115 | thinq_api=thinq_api, 116 | device_id=device_id, 117 | device_type=device_type, 118 | model_name=model_name, 119 | alias=alias, 120 | reportable=reportable, 121 | ) 122 | self.group_id = group_id 123 | self.location = location 124 | -------------------------------------------------------------------------------- /thinqconnect/devices/washcombo_main.py: -------------------------------------------------------------------------------- 1 | """ 2 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | """ 5 | from typing import Any 6 | 7 | from ..thinq_api import ThinQApi 8 | from .washcombo import WashcomboDevice 9 | 10 | 11 | class WashcomboMainDevice(WashcomboDevice): 12 | def __init__( 13 | self, 14 | thinq_api: ThinQApi, 15 | device_id: str, 16 | device_type: str, 17 | model_name: str, 18 | alias: str, 19 | group_id: str, 20 | reportable: bool, 21 | profile: dict[str, Any], 22 | ): 23 | super().__init__( 24 | thinq_api=thinq_api, 25 | device_id=device_id, 26 | device_type=device_type, 27 | model_name=model_name, 28 | alias=alias, 29 | group_id=group_id, 30 | reportable=reportable, 31 | profile=profile, 32 | location="MAIN", 33 | ) 34 | -------------------------------------------------------------------------------- /thinqconnect/devices/washcombo_mini.py: -------------------------------------------------------------------------------- 1 | """ 2 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | """ 5 | from typing import Any 6 | 7 | from ..thinq_api import ThinQApi 8 | from .washcombo import WashcomboDevice 9 | 10 | 11 | class WashcomboMiniDevice(WashcomboDevice): 12 | def __init__( 13 | self, 14 | thinq_api: ThinQApi, 15 | device_id: str, 16 | device_type: str, 17 | model_name: str, 18 | alias: str, 19 | group_id: str, 20 | reportable: bool, 21 | profile: dict[str, Any], 22 | ): 23 | super().__init__( 24 | thinq_api=thinq_api, 25 | device_id=device_id, 26 | device_type=device_type, 27 | model_name=model_name, 28 | alias=alias, 29 | group_id=group_id, 30 | reportable=reportable, 31 | profile=profile, 32 | location="MINI", 33 | ) 34 | -------------------------------------------------------------------------------- /thinqconnect/devices/washer.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """ 4 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 5 | * SPDX-License-Identifier: Apache-2.0 6 | """ 7 | from typing import Any 8 | 9 | from ..thinq_api import ThinQApi 10 | from .connect_device import ConnectDeviceProfile, ConnectMainDevice, ConnectSubDevice, ConnectSubDeviceProfile 11 | from .const import Location, Property, Resource 12 | 13 | 14 | class WasherProfile(ConnectDeviceProfile): 15 | def __init__(self, profile: dict[str, Any]): 16 | super().__init__( 17 | profile, location_map={"MAIN": Location.MAIN, "MINI": Location.MINI}, use_sub_profile_only=True 18 | ) 19 | for profile_property in profile.get("property", []): 20 | location_name = profile_property.get("location", {}).get("locationName") 21 | if location_name in self._LOCATION_MAP.keys(): 22 | attr_key = self._LOCATION_MAP[location_name] 23 | _sub_profile = WasherSubProfile(profile, location_name) 24 | self._set_sub_profile(attr_key, _sub_profile) 25 | self._set_location_properties(attr_key, _sub_profile.properties) 26 | self.generate_property_map() 27 | 28 | 29 | class WasherSubProfile(ConnectSubDeviceProfile): 30 | def __init__(self, profile: dict[str, Any], location_name: Location = None, use_sub_notification: bool = False): 31 | super().__init__( 32 | profile, 33 | location_name=location_name, 34 | resource_map={ 35 | "runState": Resource.RUN_STATE, 36 | "operation": Resource.OPERATION, 37 | "remoteControlEnable": Resource.REMOTE_CONTROL_ENABLE, 38 | "timer": Resource.TIMER, 39 | "detergent": Resource.DETERGENT, 40 | "cycle": Resource.CYCLE, 41 | }, 42 | profile_map={ 43 | "runState": {"currentState": Property.CURRENT_STATE}, 44 | "operation": { 45 | "washerOperationMode": Property.WASHER_OPERATION_MODE, 46 | }, 47 | "remoteControlEnable": {"remoteControlEnabled": Property.REMOTE_CONTROL_ENABLED}, 48 | "timer": { 49 | "remainHour": Property.REMAIN_HOUR, 50 | "remainMinute": Property.REMAIN_MINUTE, 51 | "totalHour": Property.TOTAL_HOUR, 52 | "totalMinute": Property.TOTAL_MINUTE, 53 | "relativeHourToStop": Property.RELATIVE_HOUR_TO_STOP, 54 | "relativeMinuteToStop": Property.RELATIVE_MINUTE_TO_STOP, 55 | "relativeHourToStart": Property.RELATIVE_HOUR_TO_START, 56 | "relativeMinuteToStart": Property.RELATIVE_MINUTE_TO_START, 57 | }, 58 | "detergent": {"detergentSetting": Property.DETERGENT_SETTING}, 59 | "cycle": {"cycleCount": Property.CYCLE_COUNT}, 60 | }, 61 | use_sub_notification=use_sub_notification, 62 | ) 63 | 64 | def generate_properties(self, property: list[dict[str, Any]] | dict[str, Any]) -> None: 65 | """Get properties.""" 66 | if isinstance(property, list): 67 | for location_property in property: 68 | if location_property.get("location", {}).get("locationName") != self._location_name: 69 | continue 70 | super().generate_properties(location_property) 71 | else: 72 | super().generate_properties(property) 73 | 74 | 75 | class WasherSubDevice(ConnectSubDevice): 76 | """Washer Device Sub.""" 77 | 78 | def __init__( 79 | self, 80 | profiles: WasherSubProfile, 81 | thinq_api: ThinQApi, 82 | device_id: str, 83 | device_type: str, 84 | model_name: str, 85 | alias: str, 86 | reportable: bool, 87 | location_name: Location = None, 88 | single_unit: bool = False, 89 | ): 90 | super().__init__(profiles, location_name, thinq_api, device_id, device_type, model_name, alias, reportable) 91 | 92 | @property 93 | def profiles(self) -> WasherSubProfile: 94 | return self._profiles 95 | 96 | @property 97 | def remain_time(self) -> dict: 98 | return {"hour": self.get_status(Property.REMAIN_HOUR), "minute": self.get_status(Property.REMAIN_MINUTE)} 99 | 100 | @property 101 | def total_time(self) -> dict: 102 | return {"hour": self.get_status(Property.TOTAL_HOUR), "minute": self.get_status(Property.TOTAL_MINUTE)} 103 | 104 | @property 105 | def relative_time_to_stop(self) -> dict: 106 | return { 107 | "hour": self.get_status(Property.RELATIVE_HOUR_TO_STOP), 108 | "minute": self.get_status(Property.RELATIVE_MINUTE_TO_STOP), 109 | } 110 | 111 | @property 112 | def relative_time_to_start(self) -> dict: 113 | return { 114 | "hour": self.get_status(Property.RELATIVE_HOUR_TO_START), 115 | "minute": self.get_status(Property.RELATIVE_MINUTE_TO_START), 116 | } 117 | 118 | def _set_status(self, status: dict | list, is_updated: bool = False) -> None: 119 | if isinstance(status, list): 120 | super()._set_status(status, is_updated) 121 | else: 122 | super(ConnectSubDevice, self)._set_status(status, is_updated) 123 | 124 | async def set_washer_operation_mode(self, mode: str) -> dict | None: 125 | payload = self.profiles.get_enum_attribute_payload(Property.WASHER_OPERATION_MODE, mode) 126 | return await self._do_attribute_command({"location": {"locationName": self._location_name}, **payload}) 127 | 128 | async def set_relative_hour_to_start(self, hour: str) -> dict | None: 129 | payload = self.profiles.get_range_attribute_payload(Property.RELATIVE_HOUR_TO_START, hour) 130 | return await self._do_attribute_command({"location": {"locationName": self._location_name}, **payload}) 131 | 132 | async def set_relative_hour_to_stop(self, hour: str) -> dict | None: 133 | payload = self.profiles.get_range_attribute_payload(Property.RELATIVE_HOUR_TO_STOP, hour) 134 | return await self._do_attribute_command({"location": {"locationName": self._location_name}, **payload}) 135 | 136 | 137 | class WasherDevice(ConnectMainDevice): 138 | """Washer Property.""" 139 | 140 | def __init__( 141 | self, 142 | thinq_api: ThinQApi, 143 | device_id: str, 144 | device_type: str, 145 | model_name: str, 146 | alias: str, 147 | reportable: bool, 148 | profile: dict[str, Any], 149 | ): 150 | self._sub_devices: dict[str, WasherSubDevice] = {} 151 | super().__init__( 152 | thinq_api=thinq_api, 153 | device_id=device_id, 154 | device_type=device_type, 155 | model_name=model_name, 156 | alias=alias, 157 | reportable=reportable, 158 | profiles=WasherProfile(profile=profile), 159 | sub_device_type=WasherSubDevice, 160 | ) 161 | 162 | @property 163 | def profiles(self) -> WasherProfile: 164 | return self._profiles 165 | 166 | def get_sub_device(self, location_name: Location) -> WasherSubDevice: 167 | return super().get_sub_device(location_name) 168 | -------------------------------------------------------------------------------- /thinqconnect/devices/washtower.py: -------------------------------------------------------------------------------- 1 | """ 2 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | """ 5 | from typing import Any 6 | 7 | from ..thinq_api import ThinQApi 8 | from .connect_device import ConnectBaseDevice, ConnectDeviceProfile 9 | from .const import Location, Property 10 | from .dryer import DryerDevice, DryerProfile 11 | from .washer import WasherSubDevice, WasherSubProfile 12 | 13 | 14 | class WashTowerProfile(ConnectDeviceProfile): 15 | def __init__(self, profile: dict[str, Any]): 16 | super().__init__( 17 | profile, location_map={"DRYER": Location.DRYER, "WASHER": Location.WASHER}, use_sub_profile_only=True 18 | ) 19 | for location_name, attr_key in self._LOCATION_MAP.items(): 20 | _sub_profile = ( 21 | WasherSubProfile( 22 | profile=profile.get(attr_key), location_name=Location.WASHER, use_sub_notification=True 23 | ) 24 | if location_name == "WASHER" 25 | else DryerProfile(profile=profile.get(attr_key)) 26 | ) 27 | self._set_sub_profile(attr_key, _sub_profile) 28 | self._set_location_properties(attr_key, _sub_profile.properties) 29 | self.generate_property_map() 30 | 31 | 32 | class WasherDeviceSingle(WasherSubDevice): 33 | """Washtower Washer Single Device.""" 34 | 35 | async def set_washer_operation_mode(self, operation: str) -> dict | None: 36 | payload = self.profiles.get_enum_attribute_payload(Property.WASHER_OPERATION_MODE, operation) 37 | return await self._do_attribute_command({"washer": {**payload}}) 38 | 39 | async def set_relative_hour_to_start(self, hour: int) -> dict | None: 40 | payload = self.profiles.get_range_attribute_payload(Property.RELATIVE_HOUR_TO_START, hour) 41 | return await self._do_attribute_command({"washer": {**payload}}) 42 | 43 | async def set_relative_hour_to_stop(self, hour: int) -> dict | None: 44 | payload = self.profiles.get_range_attribute_payload(Property.RELATIVE_HOUR_TO_STOP, hour) 45 | return await self._do_attribute_command({"washer": {**payload}}) 46 | 47 | 48 | class DryerDeviceSingle(DryerDevice): 49 | """Washtower Dryer Single Device.""" 50 | 51 | async def set_dryer_operation_mode(self, operation: str) -> dict | None: 52 | payload = self.profiles.get_enum_attribute_payload(Property.DRYER_OPERATION_MODE, operation) 53 | return await self._do_attribute_command({"dryer": {**payload}}) 54 | 55 | async def set_relative_time_to_start(self, hour: int) -> dict | None: 56 | payload = self.profiles.get_range_attribute_payload(Property.RELATIVE_HOUR_TO_START, hour) 57 | return await self._do_attribute_command({"dryer": {**payload}}) 58 | 59 | async def set_relative_time_to_stop(self, hour: int) -> dict | None: 60 | payload = self.profiles.get_range_attribute_payload(Property.RELATIVE_HOUR_TO_STOP, hour) 61 | return await self._do_attribute_command({"dryer": {**payload}}) 62 | 63 | 64 | class WashtowerDevice(ConnectBaseDevice): 65 | def __init__( 66 | self, 67 | thinq_api: ThinQApi, 68 | device_id: str, 69 | device_type: str, 70 | model_name: str, 71 | alias: str, 72 | reportable: bool, 73 | profile: dict[str, Any], 74 | ): 75 | super().__init__( 76 | thinq_api=thinq_api, 77 | device_id=device_id, 78 | device_type=device_type, 79 | model_name=model_name, 80 | alias=alias, 81 | reportable=reportable, 82 | profiles=WashTowerProfile(profile=profile), 83 | ) 84 | self._sub_devices: dict[str, WasherDeviceSingle | DryerDeviceSingle] = {} 85 | self.dryer = DryerDeviceSingle( 86 | thinq_api=thinq_api, 87 | device_id=device_id, 88 | device_type=device_type, 89 | model_name=model_name, 90 | alias=alias, 91 | reportable=reportable, 92 | profile=profile.get("dryer"), 93 | ) 94 | self.washer = WasherDeviceSingle( 95 | single_unit=True, 96 | thinq_api=thinq_api, 97 | device_id=device_id, 98 | device_type=device_type, 99 | model_name=model_name, 100 | alias=alias, 101 | reportable=reportable, 102 | profiles=self.profiles.get_sub_profile("washer"), 103 | ) 104 | self._sub_devices["dryer"] = self.dryer 105 | self._sub_devices["washer"] = self.washer 106 | 107 | def set_status(self, status: dict) -> None: 108 | super().set_status(status) 109 | for device_type, sub_device in self._sub_devices.items(): 110 | sub_device.set_status(status.get(device_type)) 111 | 112 | def update_status(self, status: dict) -> None: 113 | super().update_status(status) 114 | for device_type, sub_device in self._sub_devices.items(): 115 | sub_device.update_status(status.get(device_type)) 116 | 117 | def get_sub_device(self, location_name: Location) -> DryerDeviceSingle | WasherDeviceSingle: 118 | return super().get_sub_device(location_name) 119 | -------------------------------------------------------------------------------- /thinqconnect/devices/washtower_dryer.py: -------------------------------------------------------------------------------- 1 | """ 2 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | """ 5 | from typing import Any 6 | 7 | from ..thinq_api import ThinQApi 8 | from .dryer import DryerDevice 9 | 10 | 11 | class WashtowerDryerDevice(DryerDevice): 12 | @property 13 | def group_id(self) -> str: 14 | return self._group_id 15 | 16 | @group_id.setter 17 | def group_id(self, value: str): 18 | self._group_id = value 19 | 20 | def __init__( 21 | self, 22 | thinq_api: ThinQApi, 23 | device_id: str, 24 | device_type: str, 25 | model_name: str, 26 | alias: str, 27 | group_id: str, 28 | reportable: bool, 29 | profile: dict[str, Any], 30 | ): 31 | super().__init__( 32 | thinq_api=thinq_api, 33 | device_id=device_id, 34 | device_type=device_type, 35 | model_name=model_name, 36 | alias=alias, 37 | reportable=reportable, 38 | profile=profile, 39 | ) 40 | self.group_id = group_id 41 | -------------------------------------------------------------------------------- /thinqconnect/devices/washtower_washer.py: -------------------------------------------------------------------------------- 1 | """ 2 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | """ 5 | from typing import Any 6 | 7 | from ..thinq_api import ThinQApi 8 | from .washer import WasherDevice 9 | 10 | 11 | class WashtowerWasherDevice(WasherDevice): 12 | @property 13 | def group_id(self) -> str: 14 | return self._group_id 15 | 16 | @group_id.setter 17 | def group_id(self, value: str): 18 | self._group_id = value 19 | 20 | def __init__( 21 | self, 22 | thinq_api: ThinQApi, 23 | device_id: str, 24 | device_type: str, 25 | model_name: str, 26 | alias: str, 27 | group_id: str, 28 | reportable: bool, 29 | profile: dict[str, Any], 30 | ): 31 | super().__init__( 32 | thinq_api=thinq_api, 33 | device_id=device_id, 34 | device_type=device_type, 35 | model_name=model_name, 36 | alias=alias, 37 | reportable=reportable, 38 | profile=profile, 39 | ) 40 | self.group_id = group_id 41 | -------------------------------------------------------------------------------- /thinqconnect/devices/water_heater.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """ 4 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 5 | * SPDX-License-Identifier: Apache-2.0 6 | """ 7 | from typing import Any 8 | 9 | from ..thinq_api import ThinQApi 10 | from .connect_device import READABILITY, WRITABILITY, ConnectBaseDevice, ConnectDeviceProfile 11 | from .const import Property, Resource 12 | 13 | 14 | class WaterHeaterProfile(ConnectDeviceProfile): 15 | def __init__(self, profile: dict[str, Any]): 16 | super().__init__( 17 | profile=profile, 18 | resource_map={ 19 | "waterHeaterJobMode": Resource.WATER_HEATER_JOB_MODE, 20 | "operation": Resource.OPERATION, 21 | "temperatureInUnits": Resource.TEMPERATURE, 22 | }, 23 | profile_map={ 24 | "waterHeaterJobMode": {"currentJobMode": Property.CURRENT_JOB_MODE}, 25 | "operation": {"waterHeaterOperationMode": Property.WATER_HEATER_OPERATION_MODE}, 26 | "temperatureInUnits": { 27 | "currentTemperatureC": Property.CURRENT_TEMPERATURE_C, 28 | "currentTemperatureF": Property.CURRENT_TEMPERATURE_F, 29 | "targetTemperatureC": Property.TARGET_TEMPERATURE_C, 30 | "targetTemperatureF": Property.TARGET_TEMPERATURE_F, 31 | "unit": Property.TEMPERATURE_UNIT, 32 | }, 33 | }, 34 | custom_resources=["temperatureInUnits"], 35 | ) 36 | 37 | def check_attribute_writable(self, prop_attr: Property) -> bool: 38 | return prop_attr == Property.TEMPERATURE_UNIT or self._get_prop_attr(prop_attr)[WRITABILITY] 39 | 40 | def _get_attribute_payload(self, attribute: Property, value: str | int) -> dict: 41 | for resource, props in self._PROFILE.items(): 42 | for prop_key, prop_attr in props.items(): 43 | if prop_attr == attribute: 44 | return ( 45 | {resource: {prop_key: value}} 46 | if prop_key[-1:] not in ["C", "F"] 47 | else {resource: {prop_key[:-1]: value}} 48 | ) 49 | 50 | def _generate_custom_resource_properties( 51 | self, resource_key: str, resource_property: dict | list, props: dict[str, str] 52 | ) -> tuple[list[str], list[str]]: 53 | # pylint: disable=unused-argument 54 | readable_props = [] 55 | writable_props = [] 56 | 57 | if resource_key not in self._CUSTOM_RESOURCES: 58 | return readable_props, writable_props 59 | 60 | units = [] 61 | 62 | for temperatures in resource_property: 63 | unit = temperatures["unit"] 64 | for prop_key, prop_attr in props.items(): 65 | if prop_key[-1:] != unit: 66 | continue 67 | prop = self._get_properties(temperatures, prop_key[:-1]) 68 | if prop[READABILITY]: 69 | readable_props.append(str(prop_attr)) 70 | if prop[WRITABILITY]: 71 | writable_props.append(str(prop_attr)) 72 | self._set_prop_attr(prop_attr, prop) 73 | units.append(unit) 74 | 75 | prop_attr = props.get("unit") 76 | prop = self._get_readonly_enum_property(units) 77 | if prop[READABILITY]: 78 | readable_props.append(str(prop_attr)) 79 | if prop[WRITABILITY]: 80 | writable_props.append(str(prop_attr)) 81 | self._set_prop_attr(prop_attr, prop) 82 | 83 | return readable_props, writable_props 84 | 85 | 86 | class WaterHeaterDevice(ConnectBaseDevice): 87 | """WaterHeater Property.""" 88 | 89 | def __init__( 90 | self, 91 | thinq_api: ThinQApi, 92 | device_id: str, 93 | device_type: str, 94 | model_name: str, 95 | alias: str, 96 | reportable: bool, 97 | profile: dict[str, Any], 98 | ): 99 | super().__init__( 100 | thinq_api=thinq_api, 101 | device_id=device_id, 102 | device_type=device_type, 103 | model_name=model_name, 104 | alias=alias, 105 | reportable=reportable, 106 | profiles=WaterHeaterProfile(profile=profile), 107 | ) 108 | 109 | @property 110 | def profiles(self) -> WaterHeaterProfile: 111 | return self._profiles 112 | 113 | def _set_custom_resources( 114 | self, 115 | prop_key: str, 116 | attribute: str, 117 | resource_status: dict[str, str] | list[dict[str, str]], 118 | is_updated: bool = False, 119 | ) -> bool: 120 | for temperature_status in resource_status: 121 | unit = temperature_status.get("unit") 122 | if attribute is Property.TEMPERATURE_UNIT: 123 | if unit == "C": 124 | self._set_status_attr(attribute, unit) 125 | elif attribute[-1:].upper() == unit: 126 | temperature_map = self.profiles._PROFILE["temperatureInUnits"] 127 | _prop_key = None 128 | 129 | if attribute in temperature_map.values(): 130 | _prop_key = list(temperature_map.keys())[list(temperature_map.values()).index(attribute)] 131 | 132 | if not _prop_key: 133 | _attribute_value = None 134 | elif _prop_key[:-1] not in temperature_status and is_updated: 135 | continue 136 | else: 137 | _attribute_value = temperature_status.get(_prop_key[:-1]) 138 | self._set_status_attr(attribute, _attribute_value) 139 | return True 140 | 141 | async def set_current_job_mode(self, mode: str) -> dict | None: 142 | return await self.do_enum_attribute_command(Property.CURRENT_JOB_MODE, mode) 143 | 144 | async def _set_target_temperature(self, temperature: int | float, unit: str) -> dict | None: 145 | property_map = { 146 | "C": Property.TARGET_TEMPERATURE_C, 147 | "F": Property.TARGET_TEMPERATURE_F, 148 | } 149 | return await self.do_multi_attribute_command( 150 | { 151 | property_map[unit]: temperature, 152 | Property.TEMPERATURE_UNIT: unit, 153 | } 154 | ) 155 | 156 | async def set_target_temperature_c(self, temperature: int | float) -> dict | None: 157 | return await self._set_target_temperature(temperature, "C") 158 | 159 | async def set_target_temperature_f(self, temperature: int | float) -> dict | None: 160 | return await self._set_target_temperature(temperature, "F") 161 | -------------------------------------------------------------------------------- /thinqconnect/devices/water_purifier.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """ 4 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 5 | * SPDX-License-Identifier: Apache-2.0 6 | """ 7 | from typing import Any 8 | 9 | from ..thinq_api import ThinQApi 10 | from .connect_device import ConnectBaseDevice, ConnectDeviceProfile 11 | from .const import Property, Resource 12 | 13 | 14 | class WaterPurifierProfile(ConnectDeviceProfile): 15 | def __init__(self, profile: dict[str, Any]): 16 | super().__init__( 17 | profile=profile, 18 | resource_map={"runState": Resource.RUN_STATE, "waterInfo": Resource.WATER_INFO}, 19 | profile_map={ 20 | "runState": {"cockState": Property.COCK_STATE, "sterilizingState": Property.STERILIZING_STATE}, 21 | "waterInfo": {"waterType": Property.WATER_TYPE}, 22 | }, 23 | ) 24 | 25 | 26 | class WaterPurifierDevice(ConnectBaseDevice): 27 | """WaterPurifier Property.""" 28 | 29 | def __init__( 30 | self, 31 | thinq_api: ThinQApi, 32 | device_id: str, 33 | device_type: str, 34 | model_name: str, 35 | alias: str, 36 | reportable: bool, 37 | profile: dict[str, Any], 38 | ): 39 | super().__init__( 40 | thinq_api=thinq_api, 41 | device_id=device_id, 42 | device_type=device_type, 43 | model_name=model_name, 44 | alias=alias, 45 | reportable=reportable, 46 | profiles=WaterPurifierProfile(profile=profile), 47 | ) 48 | 49 | @property 50 | def profiles(self) -> WaterPurifierProfile: 51 | return self._profiles 52 | -------------------------------------------------------------------------------- /thinqconnect/devices/wine_cellar.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """ 4 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 5 | * SPDX-License-Identifier: Apache-2.0 6 | """ 7 | from typing import Any 8 | 9 | from ..thinq_api import ThinQApi 10 | from .connect_device import ( 11 | READABILITY, 12 | WRITABILITY, 13 | ConnectDeviceProfile, 14 | ConnectMainDevice, 15 | ConnectSubDevice, 16 | ConnectSubDeviceProfile, 17 | ) 18 | from .const import Location, Property, Resource 19 | 20 | 21 | class WineCellarSubProfile(ConnectSubDeviceProfile): 22 | def __init__(self, profile: dict[str, Any], location_name: Location): 23 | super().__init__( 24 | profile=profile, 25 | location_name=location_name, 26 | resource_map={"temperatureInUnits": Resource.TEMPERATURE}, 27 | profile_map={ 28 | "temperatureInUnits": { 29 | "targetTemperatureC": Property.TARGET_TEMPERATURE_C, 30 | "targetTemperatureF": Property.TARGET_TEMPERATURE_F, 31 | "unit": Property.TEMPERATURE_UNIT, 32 | }, 33 | }, 34 | custom_resources=["temperatureInUnits"], 35 | ) 36 | 37 | def _generate_custom_resource_properties( 38 | self, resource_key: str, resource_property: dict | list, props: dict[str, str] 39 | ) -> tuple[list[str], list[str]]: 40 | # pylint: disable=unused-argument 41 | readable_props = [] 42 | writable_props = [] 43 | if resource_key not in self._PROFILE.keys(): 44 | return readable_props, writable_props 45 | 46 | for _location_property in resource_property: 47 | if _location_property["locationName"] != self._location_name: 48 | continue 49 | for prop_key, prop_attr in props.items(): 50 | prop = self._get_properties(_location_property, prop_key) 51 | prop.pop("unit", None) 52 | if prop[READABILITY]: 53 | readable_props.append(str(prop_attr)) 54 | if prop[WRITABILITY]: 55 | writable_props.append(str(prop_attr)) 56 | self._set_prop_attr(prop_attr, prop) 57 | 58 | return readable_props, writable_props 59 | 60 | 61 | class WineCellarProfile(ConnectDeviceProfile): 62 | def __init__(self, profile: dict[str, Any]): 63 | super().__init__( 64 | profile=profile, 65 | location_map={ 66 | "WINE_UPPER": Location.UPPER, 67 | "WINE_MIDDLE": Location.MIDDLE, 68 | "WINE_LOWER": Location.LOWER, 69 | }, 70 | resource_map={"operation": Resource.OPERATION}, 71 | profile_map={ 72 | "operation": { 73 | "lightBrightness": Property.LIGHT_BRIGHTNESS, 74 | "optimalHumidity": Property.OPTIMAL_HUMIDITY, 75 | "sabbathMode": Property.SABBATH_MODE, 76 | "lightStatus": Property.LIGHT_STATUS, 77 | }, 78 | }, 79 | custom_resources=["temperatureInUnits"], 80 | ) 81 | 82 | for location_property in profile.get("property", {}).get("temperatureInUnits", []): 83 | location_name = location_property.get("locationName") 84 | if location_name in self._LOCATION_MAP.keys(): 85 | attr_key = self._LOCATION_MAP[location_name] 86 | _sub_profile = WineCellarSubProfile(profile, location_name) 87 | self._set_sub_profile(attr_key, _sub_profile) 88 | self._set_location_properties(attr_key, _sub_profile.properties) 89 | 90 | 91 | class WineCellarSubDevice(ConnectSubDevice): 92 | """WineCellar Device Sub.""" 93 | 94 | def __init__( 95 | self, 96 | profiles: WineCellarSubProfile, 97 | location_name: Location, 98 | thinq_api: ThinQApi, 99 | device_id: str, 100 | device_type: str, 101 | model_name: str, 102 | alias: str, 103 | reportable: bool, 104 | ): 105 | super().__init__( 106 | profiles, 107 | location_name, 108 | thinq_api, 109 | device_id, 110 | device_type, 111 | model_name, 112 | alias, 113 | reportable, 114 | is_single_resource=True, 115 | ) 116 | 117 | @property 118 | def profiles(self) -> WineCellarSubProfile: 119 | return self._profiles 120 | 121 | def _set_custom_resources( 122 | self, 123 | prop_key: str, 124 | attribute: str, 125 | resource_status: dict[str, str] | list[dict[str, str]], 126 | is_updated: bool = False, 127 | ) -> bool: 128 | if is_updated and attribute in [Property.TARGET_TEMPERATURE_C, Property.TARGET_TEMPERATURE_F]: 129 | current_unit = resource_status.get("unit") or self.get_status(Property.TEMPERATURE_UNIT) 130 | if attribute[-1:].upper() == current_unit: 131 | self._set_status_attr(attribute, value=resource_status.get(prop_key)) 132 | return True 133 | return False 134 | 135 | async def _set_target_temperature(self, temperature: int | float, unit: str) -> dict | None: 136 | _resource_key = "temperatureInUnits" 137 | _target_temperature_key = self.get_property_key(_resource_key, "targetTemperature" + unit) 138 | 139 | _payload = self.profiles.get_range_attribute_payload(_target_temperature_key, temperature) 140 | _payload[_resource_key] = dict( 141 | { 142 | "locationName": self._location_name, 143 | }, 144 | **(_payload[_resource_key]), 145 | ) 146 | return await self._do_attribute_command(_payload) 147 | 148 | async def set_target_temperature_c(self, temperature: int | float) -> dict | None: 149 | return await self._set_target_temperature(temperature, "C") 150 | 151 | async def set_target_temperature_f(self, temperature: int | float) -> dict | None: 152 | return await self._set_target_temperature(temperature, "F") 153 | 154 | 155 | class WineCellarDevice(ConnectMainDevice): 156 | """WineCellar Property.""" 157 | 158 | def __init__( 159 | self, 160 | thinq_api: ThinQApi, 161 | device_id: str, 162 | device_type: str, 163 | model_name: str, 164 | alias: str, 165 | reportable: bool, 166 | profile: dict[str, Any], 167 | ): 168 | self._sub_devices: dict[str, WineCellarSubDevice] = {} 169 | super().__init__( 170 | thinq_api=thinq_api, 171 | device_id=device_id, 172 | device_type=device_type, 173 | model_name=model_name, 174 | alias=alias, 175 | reportable=reportable, 176 | profiles=WineCellarProfile(profile=profile), 177 | sub_device_type=WineCellarSubDevice, 178 | ) 179 | 180 | @property 181 | def profiles(self) -> WineCellarProfile: 182 | return self._profiles 183 | 184 | def get_sub_device(self, location_name: Location) -> WineCellarSubDevice: 185 | return super().get_sub_device(location_name) 186 | 187 | async def set_light_brightness(self, brightness_input: str) -> dict | None: 188 | return await self.do_enum_attribute_command(Property.LIGHT_BRIGHTNESS, brightness_input) 189 | 190 | async def set_optimal_humidity(self, humidity_input: str) -> dict | None: 191 | return await self.do_enum_attribute_command(Property.OPTIMAL_HUMIDITY, humidity_input) 192 | 193 | async def set_light_status(self, status_input: int) -> dict | None: 194 | return await self.do_range_attribute_command(Property.LIGHT_STATUS, status_input) 195 | -------------------------------------------------------------------------------- /thinqconnect/integration/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | """ 5 | 6 | # Module integration. 7 | 8 | from .homeassistant.api import ( 9 | HABridge, 10 | async_get_ha_bridge_list, 11 | NotConnectedDeviceError, 12 | ) 13 | from .homeassistant.property import ActiveMode 14 | from .homeassistant.specification import ( 15 | ExtendedProperty, 16 | ThinQPropertyEx, 17 | TimerProperty, 18 | ) 19 | from .homeassistant.state import DeviceState, PropertyState 20 | 21 | __all__ = [ 22 | "ActiveMode", 23 | "DeviceState", 24 | "ExtendedProperty", 25 | "HABridge", 26 | "NotConnectedDeviceError", 27 | "PropertyState", 28 | "ThinQPropertyEx", 29 | "TimerProperty", 30 | "async_get_ha_bridge_list", 31 | ] 32 | -------------------------------------------------------------------------------- /thinqconnect/integration/homeassistant/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | """ 5 | 6 | # Module homeassistant. 7 | -------------------------------------------------------------------------------- /thinqconnect/integration/homeassistant/property.py: -------------------------------------------------------------------------------- 1 | """ 2 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | """ 5 | 6 | # The extended property interface for Home Assistant. 7 | 8 | from __future__ import annotations 9 | 10 | import inspect 11 | from dataclasses import dataclass 12 | from enum import Enum, auto 13 | from typing import Any, Awaitable, Callable 14 | 15 | from thinqconnect import ( 16 | PROPERTY_READABLE, 17 | PROPERTY_WRITABLE, 18 | ConnectBaseDevice, 19 | ) 20 | from thinqconnect.devices.const import Location 21 | from thinqconnect.devices.const import Property as ThinQProperty 22 | 23 | 24 | class ActiveMode(Enum): 25 | """A list of mode that represents read-write mode of property/state.""" 26 | 27 | READ_ONLY = auto() 28 | WRITE_ONLY = auto() 29 | READABLE = auto() 30 | WRITABLE = auto() 31 | READ_WRITE = auto() 32 | 33 | 34 | @dataclass(kw_only=True, frozen=True) 35 | class PropertyOption: 36 | """A class tha contains options for creating property holder.""" 37 | 38 | alt_setter: Callable[[ConnectBaseDevice, Any], Awaitable[None]] | None = None 39 | 40 | 41 | class PropertyHolder: 42 | """A class that represents lg thinq property.""" 43 | 44 | def __init__( 45 | self, 46 | key: str, 47 | device_api: ConnectBaseDevice, 48 | profile: dict[str, Any], 49 | *, 50 | location: str | None = None, 51 | option: PropertyOption | None = None, 52 | ) -> None: 53 | """Initialize a property.""" 54 | super().__init__() 55 | 56 | self.key = key 57 | self.api = ( 58 | device_api.get_sub_device(Location(location)) 59 | if location is not None and location in Location 60 | else None 61 | ) or device_api 62 | self.profile = profile or {} 63 | self.location = location 64 | self.setter = self._retrieve_setter() 65 | self.option = option or PropertyOption() 66 | self.data_type = self.profile.get("type") 67 | self.rw_mode: str | None = None 68 | 69 | @property 70 | def options(self) -> list[str] | None: 71 | """Retrieve a list of options from the given profile.""" 72 | data = self._get_profile_data() 73 | 74 | if self.data_type == "enum" and isinstance(data, list): 75 | return [item.lower() if isinstance(item, str) else item for item in data] 76 | 77 | if self.data_type == "boolean" and data is True: 78 | return ["false", "true"] 79 | 80 | if self.key == ThinQProperty.WIND_STEP and self.data_type == "range": 81 | if self.min is not None and self.max is not None: 82 | return [str(i) for i in range(int(self.min), int(self.max) + 1)] 83 | 84 | return None 85 | 86 | @property 87 | def min(self) -> float | None: 88 | """Return the minimum value.""" 89 | if isinstance(data := self._get_profile_data(), dict): 90 | return data.get("min") 91 | 92 | return None 93 | 94 | @property 95 | def max(self) -> float | None: 96 | """Return the maximum value.""" 97 | if isinstance(data := self._get_profile_data(), dict): 98 | return data.get("max") 99 | 100 | return None 101 | 102 | @property 103 | def step(self) -> float | None: 104 | """Return the step value.""" 105 | if isinstance(data := self._get_profile_data(), dict): 106 | return data.get("step") 107 | 108 | return None 109 | 110 | @property 111 | def unit(self) -> str | None: 112 | """Return the unit.""" 113 | if isinstance(unit := self.profile.get("unit"), dict) and isinstance( 114 | unit_value := unit.get("value"), dict 115 | ): 116 | unit = unit_value.get(PROPERTY_WRITABLE) or unit_value.get( 117 | PROPERTY_READABLE 118 | ) 119 | 120 | return str(unit) if unit else None 121 | 122 | def _get_profile_data(self) -> Any | None: 123 | """Return the data of profile.""" 124 | if self.rw_mode: 125 | return self.profile.get(self.rw_mode) 126 | 127 | return self.profile.get(PROPERTY_WRITABLE) or self.profile.get( 128 | PROPERTY_READABLE 129 | ) 130 | 131 | def _retrieve_setter(self) -> Callable[[Any], Awaitable[Any]] | None: 132 | """Retrieve the setter method.""" 133 | for name, func in inspect.getmembers(self.api): 134 | if inspect.iscoroutinefunction(func) and name == f"set_{self.key}": 135 | return func 136 | 137 | return None 138 | 139 | def can_activate(self, active_mode: ActiveMode | None) -> bool: 140 | """Check whether the requested active mode is available or not.""" 141 | readable = self.profile.get(PROPERTY_READABLE, False) 142 | writable = self.profile.get(PROPERTY_WRITABLE, False) 143 | 144 | if active_mode is ActiveMode.READ_ONLY: 145 | return readable and not writable 146 | 147 | if active_mode is ActiveMode.WRITE_ONLY: 148 | return not readable and writable 149 | 150 | if active_mode is ActiveMode.READABLE: 151 | return readable 152 | 153 | if active_mode is ActiveMode.WRITABLE: 154 | return writable 155 | 156 | if active_mode is ActiveMode.READ_WRITE: 157 | return readable and writable 158 | 159 | return True 160 | 161 | def set_rw_mode(self, active_mode: ActiveMode | None) -> None: 162 | """Set read-write mode from the given active mode.""" 163 | if not self.can_activate(active_mode): 164 | return 165 | 166 | if active_mode in (ActiveMode.READ_ONLY, ActiveMode.READABLE): 167 | self.rw_mode = PROPERTY_READABLE 168 | else: 169 | self.rw_mode = PROPERTY_WRITABLE 170 | 171 | def get_value(self) -> Any: 172 | """Return the value of property.""" 173 | status = self.api.get_status(self.key) # type: ignore[arg-type] 174 | if status is None: 175 | return None 176 | 177 | if self.data_type in ["enum", "boolean"]: 178 | return str(status).lower() 179 | 180 | if self.key == ThinQProperty.WIND_STEP and self.data_type == "range": 181 | return str(status) 182 | 183 | return status 184 | 185 | def get_value_as_bool(self) -> bool: 186 | """Return the value as boolean type.""" 187 | return PropertyHolder.to_boolean_value(self.get_value()) 188 | 189 | def get_unit(self) -> Any: 190 | """Return the unit on runtime.""" 191 | if self.key in ThinQProperty and isinstance( 192 | value := self.api.get_status(ThinQProperty(self.key)), dict 193 | ): 194 | return value.get("unit") 195 | 196 | return self.unit 197 | 198 | async def async_set(self, value: Any) -> None: 199 | """Set the value of property.""" 200 | value = self._convert_value(value) 201 | 202 | if self.option.alt_setter is not None: 203 | await self.option.alt_setter(self.api, value) 204 | elif self.setter is not None: 205 | await self.setter(value) 206 | 207 | def _convert_value(self, value: Any) -> Any: 208 | """Convert the given value to acceptable value for thinq api.""" 209 | if ( 210 | isinstance(value, (int, float)) 211 | and self.step is not None 212 | and self.step.is_integer() 213 | ): 214 | return int(value) 215 | 216 | if isinstance(value, str): 217 | if self.data_type == "enum": 218 | return value.upper() 219 | if self.data_type == "range": 220 | return int(value) 221 | if self.data_type == "boolean" and value in ("false", "true"): 222 | return value == "true" 223 | 224 | return value 225 | 226 | @staticmethod 227 | def to_boolean_value(value: Any) -> bool: 228 | """Convert the given value to boolean type.""" 229 | if not value: 230 | return False 231 | 232 | if isinstance(value, str): 233 | if value.lower() in ["power_off", "false", "off"]: 234 | return False 235 | elif value.lower() in ["power_on", "true", "on"]: 236 | return True 237 | else: 238 | return False 239 | 240 | return bool(value) 241 | 242 | @staticmethod 243 | def is_valid_range(min: float | None, max: float | None) -> bool: 244 | """Check whether the given values are valid range or not.""" 245 | return min is not None and max is not None and min < max 246 | 247 | def dump(self) -> str: 248 | """Dump the current status.""" 249 | messages: list[str] = [] 250 | messages.append(f"{str(self)}<") 251 | messages.append(f"type={self.data_type},") 252 | messages.append(f"options={self.options},") 253 | messages.append(f"range={self.min}-{self.max}:{self.step},") 254 | messages.append(f"unit(profile)={self.unit},unit(runtime)={self.get_unit()},") 255 | messages.append(f"value={self.get_value()}>") 256 | return "".join(messages) 257 | 258 | def __str__(self) -> str: 259 | """Return the short string.""" 260 | if self.location: 261 | return f"PropertyHolder({self.api.alias}/{self.location}:{self.key})" 262 | 263 | return f"PropertyHolder({self.api.alias}:{self.key}:)" 264 | -------------------------------------------------------------------------------- /thinqconnect/integration/homeassistant/temperature.py: -------------------------------------------------------------------------------- 1 | """ 2 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 3 | * SPDX-License-Identifier: Apache-2.0 4 | """ 5 | 6 | # Temperature logics. 7 | 8 | from abc import ABC 9 | from typing import Any, TypeAlias 10 | 11 | from .property import PropertyHolder 12 | 13 | # TypeAlias for a set of temperature holders. 14 | # 15 | # holders = { 16 | # "value": PropertyHolder(key=cool_target_temperature_c) 17 | # "min": PropertyHolder(key=cool_min_temperature_c) 18 | # "max": PropertyHolder(key=cool_max_temperature_c) 19 | # } 20 | TemperatureHolders: TypeAlias = dict[str, PropertyHolder] 21 | 22 | # TypeAlias for a group of temperature holders. 23 | # 24 | # group = { 25 | # "C": [holders_celsius], 26 | # "F": [holders_fahrenheit], 27 | # "_": [holders_legacy] 28 | # } 29 | 30 | TemperatureGroup: TypeAlias = dict[str, TemperatureHolders] 31 | 32 | # TypeAlias for hvac mode and temperature map. 33 | # 34 | # temperature/hvac_map = { 35 | # "cool": [group_cool], 36 | # "heat": [group_heat], 37 | # "auto": [group_auto], 38 | # } 39 | TemperatureHvacMap: TypeAlias = dict[str, TemperatureGroup] 40 | 41 | 42 | class ClimateTemperatureGroup: 43 | """A group that contains a set of temperature holders to control climate.""" 44 | 45 | def __init__( 46 | self, 47 | current_temp_hvac_map: TemperatureHvacMap, 48 | target_temp_hvac_map: TemperatureHvacMap, 49 | target_temp_low_hvac_map: TemperatureHvacMap, 50 | target_temp_high_hvac_map: TemperatureHvacMap, 51 | unit_holder: PropertyHolder | None = None, 52 | ) -> None: 53 | """Initialize.""" 54 | self.current_temp_hvac_map = current_temp_hvac_map 55 | self.target_temp_hvac_map = target_temp_hvac_map 56 | self.target_temp_low_hvac_map = target_temp_low_hvac_map 57 | self.target_temp_high_hvac_map = target_temp_high_hvac_map 58 | self.unit_holder = unit_holder 59 | 60 | 61 | # TypeAlias for preset mode and temperature/hvac_map. 62 | # 63 | # hvac_map = { 64 | # "air": [temperature/hvac_map_AIR], 65 | # "water/in_water": [temperature/hvac_map_WATER_IN], 66 | # "water/out_water": [temperature/hvac_map_WATER_OUT], 67 | # } 68 | ClimateTemperatureMap: TypeAlias = dict[str, ClimateTemperatureGroup] 69 | 70 | 71 | class TemperatureHelperBase(ABC): 72 | """The base temperature helper class.""" 73 | 74 | def __init__( 75 | self, unit_holder: PropertyHolder | None, use_preferred_unit: bool 76 | ) -> None: 77 | """Set up.""" 78 | self.unit_holder = unit_holder 79 | self.use_preferred_unit = use_preferred_unit 80 | self.current_holder: PropertyHolder | None = None 81 | self.value: float | None = None 82 | self.unit: str | None = None 83 | self.min_value: float | None = None 84 | self.max_value: float | None = None 85 | 86 | @property 87 | def is_active(self) -> bool: 88 | """Check whether this helper is active or not.""" 89 | return self.current_holder is not None 90 | 91 | @property 92 | def min(self) -> float | None: 93 | """Return the minimum value.""" 94 | if self.current_holder is None: 95 | return None 96 | 97 | if PropertyHolder.is_valid_range(self.min_value, self.max_value): 98 | return self.min_value 99 | 100 | return self.current_holder.min 101 | 102 | @property 103 | def max(self) -> float | None: 104 | """Return the maximum value.""" 105 | if self.current_holder is None: 106 | return None 107 | 108 | if PropertyHolder.is_valid_range(self.min_value, self.max_value): 109 | return self.max_value 110 | 111 | return self.current_holder.max 112 | 113 | @property 114 | def step(self) -> float | None: 115 | """Return the step value.""" 116 | if self.current_holder is None: 117 | return None 118 | 119 | return self.current_holder.step 120 | 121 | def _get_target_unit(self, preferred_unit: str | None = None) -> str | None: 122 | """Return the target unit to calculate.""" 123 | if self.use_preferred_unit and preferred_unit is not None: 124 | return preferred_unit 125 | 126 | if self.unit_holder is not None and isinstance( 127 | target_unit := self.unit_holder.get_value(), str 128 | ): 129 | return target_unit.upper() 130 | 131 | return None 132 | 133 | def _update( 134 | self, holder_group: TemperatureGroup | None, preferred_unit: str | None = None 135 | ) -> float | None: 136 | """Internal update logic.""" 137 | holder: PropertyHolder | None = None 138 | value: Any = None 139 | unit: str | None = None 140 | min_value: float | None = None 141 | max_value: float | None = None 142 | 143 | if holder_group: 144 | if ( 145 | preferred_unit is not None 146 | and (holders := holder_group.get(preferred_unit)) is not None 147 | and (holder := holders.get("value")) is not None 148 | ): 149 | value = holder.get_value() 150 | unit = preferred_unit 151 | 152 | if (min_holder := holders.get("min")) is not None: 153 | min_value = min_holder.get_value() 154 | if (max_holder := holders.get("max")) is not None: 155 | max_value = max_holder.get_value() 156 | elif (holders := holder_group.get("_")) is not None and ( 157 | holder := holders.get("value") 158 | ) is not None: 159 | value = holder.get_value() 160 | unit = holder.get_unit() 161 | 162 | if (min_holder := holders.get("min")) is not None: 163 | min_value = min_holder.get_value() 164 | if (max_holder := holders.get("max")) is not None: 165 | max_value = max_holder.get_value() 166 | 167 | self.current_holder = holder 168 | self.value = value 169 | self.unit = unit or preferred_unit 170 | self.min_value = min_value 171 | self.max_value = max_value 172 | return value 173 | 174 | 175 | class TemperatureHelper(TemperatureHelperBase): 176 | """A helper class that select a temperature property holder that is valid.""" 177 | 178 | def __init__( 179 | self, 180 | holder_group: TemperatureGroup, 181 | *, 182 | unit_holder: PropertyHolder | None = None, 183 | use_preferred_unit: bool = False, 184 | ) -> None: 185 | """Set up.""" 186 | super().__init__(unit_holder, use_preferred_unit) 187 | 188 | self.holder_group = holder_group 189 | 190 | def update(self, preferred_unit: str | None = None) -> float | None: 191 | """Actually update holder, value and unit.""" 192 | return self._update(self.holder_group, self._get_target_unit(preferred_unit)) 193 | 194 | 195 | class ClimateTemperatureHelper(TemperatureHelperBase): 196 | """A helper class that select a temperature property holder that is valid.""" 197 | 198 | def __init__( 199 | self, 200 | hvac_temperature_map: TemperatureHvacMap, 201 | *, 202 | unit_holder: PropertyHolder | None = None, 203 | use_preferred_unit: bool = False, 204 | use_wildcard_holder: bool = False, 205 | ) -> None: 206 | """Set up.""" 207 | super().__init__(unit_holder, use_preferred_unit) 208 | 209 | self.hvac_temperature_map = hvac_temperature_map 210 | self.use_wildcard_holder = use_wildcard_holder 211 | 212 | def update(self, hvac_mode: str, preferred_unit: str | None) -> float | None: 213 | """Update the current holder and then return the value.""" 214 | # In this case, use only one holder(key: "_") regardless of hvac mode. 215 | if self.use_wildcard_holder: 216 | hvac_mode = "_" 217 | 218 | return self._update( 219 | self.hvac_temperature_map.get(hvac_mode), 220 | self._get_target_unit(preferred_unit), 221 | ) 222 | 223 | def get_holder(self, hvac_mode: str) -> PropertyHolder | None: 224 | """Return the specified holder.""" 225 | unit = self.unit or "_" 226 | if (holder_group := self.hvac_temperature_map.get(hvac_mode)) is not None and ( 227 | holders := holder_group.get(unit) 228 | ) is not None: 229 | return holders.get("value") 230 | 231 | return None 232 | -------------------------------------------------------------------------------- /thinqconnect/mqtt_client.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """ 4 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 5 | * SPDX-License-Identifier: Apache-2.0 6 | """ 7 | """Support for LG ThinQ Connect API.""" 8 | 9 | import asyncio 10 | import logging 11 | import re 12 | from enum import Enum 13 | from typing import Callable 14 | 15 | from aiohttp import ClientTimeout, request 16 | from awscrt import io, mqtt 17 | from awsiot import mqtt_connection_builder 18 | from OpenSSL import crypto 19 | 20 | from .thinq_api import ThinQApi 21 | 22 | FILE_ROOT_CA = "AmazonRootCA1.pem" 23 | ROOT_CA_REPOSITORY = "https://www.amazontrust.com/repository" 24 | PRIVATE_KEY_SIZE = 2048 25 | 26 | _LOGGER = logging.getLogger(__name__) 27 | 28 | CLIENT_BODY = {"type": "MQTT", "service-code": "SVC202", "device-type": "607", "allowExist": True} 29 | 30 | 31 | class ClientConnectionState(str, Enum): 32 | """client connection state.""" 33 | 34 | CLIENT_CONNECTED = "client_connected" 35 | CLIENT_DISCONNECTED = "client_disconnected" 36 | 37 | def __str__(self): 38 | return self.name 39 | 40 | 41 | class ThinQMQTTClient: 42 | """A class for LG Connect-Client API calls.""" 43 | 44 | def __init__( 45 | self, 46 | thinq_api: ThinQApi, 47 | client_id: str, 48 | on_message_received: Callable, 49 | on_connection_interrupted: Callable = None, 50 | on_connection_success: Callable = None, 51 | on_connection_failure: Callable = None, 52 | on_connection_closed: Callable = None, 53 | ): 54 | self._thinq_api = thinq_api 55 | self._client_id = client_id 56 | self._mqtt_connection: mqtt.Connection = None 57 | self._on_message_received = on_message_received 58 | self._on_connection_interrupted = on_connection_interrupted 59 | self._on_connection_success = on_connection_success 60 | self._on_connection_failure = on_connection_failure 61 | self._on_connection_closed = on_connection_closed 62 | self._state: str = ClientConnectionState.CLIENT_DISCONNECTED 63 | 64 | def __await__(self): 65 | yield from self.async_init().__await__() 66 | return self 67 | 68 | async def async_init(self): 69 | route_response = await self._thinq_api.async_get_route() 70 | self._mqtt_server = route_response.get("mqttServer").replace("mqtts://", "").split(":", maxsplit=1)[0] 71 | 72 | @property 73 | def mqtt_server(self) -> str: 74 | return self._mqtt_server 75 | 76 | @mqtt_server.setter 77 | def mqtt_server(self, mqtt_server: str): 78 | self._mqtt_server = mqtt_server 79 | 80 | @property 81 | def bytes_root_ca(self) -> bytes: 82 | """Return root CA certificate bytes.""" 83 | return self._bytes_root_ca 84 | 85 | @bytes_root_ca.setter 86 | def bytes_root_ca(self, bytes_root_ca: bytes): 87 | """Set root CA certificate bytes.""" 88 | self._bytes_root_ca = bytes_root_ca 89 | 90 | @property 91 | def bytes_private_key(self) -> bytes: 92 | """Return private key bytes.""" 93 | return self._bytes_private_key 94 | 95 | @bytes_private_key.setter 96 | def bytes_private_key(self, bytes_private_key: bytes): 97 | """Set private key bytes.""" 98 | self._bytes_private_key = bytes_private_key 99 | 100 | @property 101 | def bytes_certificate(self) -> bytes: 102 | """Return client certificate bytes.""" 103 | return self._bytes_certificate 104 | 105 | @bytes_certificate.setter 106 | def bytes_certificate(self, bytes_certificate: bytes): 107 | """Set client certificate bytes.""" 108 | self._bytes_certificate = bytes_certificate 109 | 110 | @property 111 | def csr_str(self) -> str: 112 | """Return client CSR string.""" 113 | return self._csr_str 114 | 115 | @csr_str.setter 116 | def csr_str(self, csr_str: str): 117 | """Set client CSR string.""" 118 | self._csr_str = csr_str 119 | 120 | @property 121 | def topic_subscription(self) -> str: 122 | """Return subscription topics.""" 123 | return self._topic_subscription 124 | 125 | @topic_subscription.setter 126 | def topic_subscription(self, topic_subscription: str): 127 | """Set subscription topics.""" 128 | self._topic_subscription = topic_subscription 129 | 130 | @property 131 | def is_connected(self) -> bool: 132 | """Return true if the client is connected to the mqtt.""" 133 | return self._state == ClientConnectionState.CLIENT_CONNECTED 134 | 135 | async def async_prepare_mqtt(self) -> bool: 136 | """Prepare for MQTT connection.""" 137 | await self._thinq_api.async_post_client_register(payload=CLIENT_BODY) 138 | if not await self.generate_csr(): 139 | return False 140 | if not await self.issue_certificate(): 141 | return False 142 | return True 143 | 144 | async def _on_disconnect(self) -> None: 145 | """Handle client disconnect.""" 146 | result = await self._thinq_api.async_delete_client_register(payload=CLIENT_BODY) 147 | _LOGGER.debug("Delete client register, result: %s", result) 148 | self._state = ClientConnectionState.CLIENT_DISCONNECTED 149 | 150 | async def generate_csr(self) -> bool: 151 | """Create CSR.""" 152 | cert_data = await self._get_root_certificate() 153 | self.bytes_root_ca = cert_data.encode("utf-8") 154 | 155 | if cert_data is None: 156 | _LOGGER.error("Root certification download failed") 157 | return False 158 | 159 | key = crypto.PKey() 160 | key.generate_key(crypto.TYPE_RSA, PRIVATE_KEY_SIZE) 161 | key_pem = crypto.dump_privatekey(crypto.FILETYPE_PEM, key).decode("utf-8") 162 | self.bytes_private_key = key_pem.encode("utf-8") 163 | 164 | csr = crypto.X509Req() 165 | csr.get_subject().CN = "lg_thinq" 166 | csr.set_pubkey(key) 167 | csr.sign(key, "sha512") 168 | 169 | csr_pem = crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr).decode(encoding="utf-8") 170 | self.csr_str = ( 171 | re.search( 172 | r"-+BEGIN CERTIFICATE REQUEST-+\s+(.*?)\s+-+END CERTIFICATE REQUEST-+", 173 | csr_pem, 174 | flags=re.DOTALL, 175 | ) 176 | .group(1) 177 | .strip() 178 | .replace("\n", "") 179 | ) 180 | return True 181 | 182 | async def issue_certificate(self) -> bool: 183 | """Create client certificate.""" 184 | if not self.csr_str: 185 | _LOGGER.error("No device specified. skip.") 186 | return False 187 | body = { 188 | "service-code": "SVC202", 189 | "csr": self.csr_str, 190 | } 191 | _LOGGER.info("Request client certificate, body: %s", body) 192 | 193 | response = await self._thinq_api.async_post_client_certificate(body) 194 | if response is None: 195 | return False 196 | 197 | _LOGGER.debug("Request client certificate, result: %s", response) 198 | certificate_pem: str = response.get("result").get("certificatePem") 199 | subscriptions: list[str] = response.get("result").get("subscriptions") 200 | if certificate_pem is None or subscriptions is None: 201 | return False 202 | 203 | self.bytes_certificate = certificate_pem.encode("utf-8") 204 | self.topic_subscription = subscriptions[0] 205 | 206 | return True 207 | 208 | async def _get_root_certificate(self, timeout: int = 15) -> str | None: 209 | """Get aws root CA certificate.""" 210 | url = f"{ROOT_CA_REPOSITORY}/{FILE_ROOT_CA}" 211 | _LOGGER.debug("get_root_certificate. url=%s", url) 212 | client_timeout = ClientTimeout(total=timeout) 213 | async with request("GET", url=url, timeout=client_timeout) as response: 214 | result = await response.text() 215 | if response.status != 200 or "error" in result: 216 | _LOGGER.error( 217 | "Failed to call API: status=%s, result=%s", 218 | response.status, 219 | result, 220 | ) 221 | return None 222 | return result 223 | 224 | async def async_connect_mqtt(self) -> None: 225 | """Start push/event subscribes.""" 226 | 227 | def connect_mqtt(): 228 | event_loop_group = io.EventLoopGroup(1) 229 | host_resolver = io.DefaultHostResolver(event_loop_group) 230 | client_bootstrap = io.ClientBootstrap(event_loop_group, host_resolver) 231 | mqtt_connection = mqtt_connection_builder.mtls_from_bytes( 232 | endpoint=self._mqtt_server, 233 | port=None, 234 | cert_bytes=self.bytes_certificate, 235 | pri_key_bytes=self.bytes_private_key, 236 | client_bootstrap=client_bootstrap, 237 | ca_bytes=self.bytes_root_ca, 238 | on_connection_interrupted=self._on_connection_interrupted, 239 | on_connection_success=self._on_connection_success, 240 | on_connection_failure=self._on_connection_failure, 241 | on_connection_closed=self._on_connection_closed, 242 | client_id=self._client_id, 243 | clean_session=False, 244 | keep_alive_secs=6, 245 | http_proxy_options=None, 246 | ) 247 | return mqtt_connection 248 | 249 | loop = asyncio.get_running_loop() 250 | mqtt_connection = await loop.run_in_executor(None, connect_mqtt) 251 | 252 | _LOGGER.debug( 253 | "Connecting to endpoint=%s, client_id: %s", 254 | self.mqtt_server, 255 | self._client_id, 256 | ) 257 | 258 | try: 259 | connect_future = mqtt_connection.connect() 260 | connect_result = connect_future.result() 261 | _LOGGER.debug( 262 | "Connect with session_present : %s", 263 | connect_result["session_present"], 264 | ) 265 | self._state == ClientConnectionState.CLIENT_CONNECTED 266 | except Exception as err: 267 | _LOGGER.error("Failed to connect endpoint: %s", err) 268 | return None 269 | self._state = ClientConnectionState.CLIENT_CONNECTED 270 | self._mqtt_connection = mqtt_connection 271 | if self._mqtt_connection is not None: 272 | try: 273 | subscribe_future, _ = self._mqtt_connection.subscribe( 274 | topic=self.topic_subscription, 275 | qos=mqtt.QoS.AT_LEAST_ONCE, 276 | callback=self._on_message_received, 277 | ) 278 | subscribe_future.result() 279 | _LOGGER.debug("Complete subscription!") 280 | except Exception as err: 281 | _LOGGER.error("Failed to subscription: %s", err) 282 | 283 | async def async_disconnect(self) -> None: 284 | """Unregister client and disconnects handlers""" 285 | self._mqtt_connection.unsubscribe(topic=self.topic_subscription) 286 | await self._on_disconnect() 287 | -------------------------------------------------------------------------------- /thinqconnect/thinq_api.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | """ 4 | * SPDX-FileCopyrightText: Copyright 2024 LG Electronics Inc. 5 | * SPDX-License-Identifier: Apache-2.0 6 | """ 7 | """Support for LG ThinQ Connect API.""" 8 | 9 | import base64 10 | import logging 11 | import uuid 12 | from typing import Any 13 | 14 | from aiohttp import ClientResponse, ClientSession 15 | from aiohttp.hdrs import METH_DELETE, METH_GET, METH_POST 16 | from aiohttp.typedefs import StrOrURL 17 | 18 | from .const import API_KEY 19 | from .country import get_region_from_country 20 | 21 | _LOGGER = logging.getLogger(__name__) 22 | 23 | 24 | class ThinQAPIErrorCodes: 25 | """The class that represents the error codes for LG ThinQ Connect API.""" 26 | 27 | UNKNOWN_ERROR = "0000" 28 | BAD_REQUEST = "1000" 29 | MISSING_PARAMETERS = "1101" 30 | UNACCEPTABLE_PARAMETERS = "1102" 31 | INVALID_TOKEN = "1103" 32 | INVALID_MESSAGE_ID = "1104" 33 | NOT_REGISTERED_ADMIN = "1201" 34 | NOT_REGISTERED_USER = "1202" 35 | NOT_REGISTERED_SERVICE = "1203" 36 | NOT_SUBSCRIBED_EVENT = "1204" 37 | NOT_REGISTERED_DEVICE = "1205" 38 | NOT_SUBSCRIBED_PUSH = "1206" 39 | ALREADY_SUBSCRIBED_PUSH = "1207" 40 | NOT_REGISTERED_SERVICE_BY_ADMIN = "1208" 41 | NOT_REGISTERED_USER_IN_SERVICE = "1209" 42 | NOT_REGISTERED_DEVICE_IN_SERVICE = "1210" 43 | NOT_REGISTERED_DEVICE_BY_USER = "1211" 44 | NOT_OWNED_DEVICE = "1212" 45 | NOT_REGISTERED_DEVICE = "1213" 46 | NOT_SUBSCRIBABLE_DEVICE = "1214" 47 | INCORRECT_HEADER = "1216" 48 | ALREADY_DEVICE_DELETED = "1217" 49 | INVALID_TOKEN_AGAIN = "1218" 50 | NOT_SUPPORTED_MODEL = "1219" 51 | NOT_SUPPORTED_FEATURE = "1220" 52 | NOT_SUPPORTED_PRODUCT = "1221" 53 | NOT_CONNECTED_DEVICE = "1222" 54 | INVALID_STATUS_DEVICE = "1223" 55 | INVALID_DEVICE_ID = "1224" 56 | DUPLICATE_DEVICE_ID = "1225" 57 | INVALID_SERVICE_KEY = "1301" 58 | NOT_FOUND_TOKEN = "1302" 59 | NOT_FOUND_USER = "1303" 60 | NOT_ACCEPTABLE_TERMS = "1304" 61 | NOT_ALLOWED_API = "1305" 62 | EXCEEDED_API_CALLS = "1306" 63 | NOT_SUPPORTED_COUNTRY = "1307" 64 | NO_CONTROL_AUTHORITY = "1308" 65 | NOT_ALLOWED_API_AGAIN = "1309" 66 | NOT_SUPPORTED_DOMAIN = "1310" 67 | BAD_REQUEST_FORMAT = "1311" 68 | EXCEEDED_NUMBER_OF_EVENT_SUBSCRIPTION = "1312" 69 | INTERNAL_SERVER_ERROR = "2000" 70 | NOT_SUPPORTED_MODEL_AGAIN = "2101" 71 | NOT_PROVIDED_FEATURE = "2201" 72 | NOT_SUPPORTED_PRODUCT_AGAIN = "2202" 73 | NOT_EXISTENT_MODEL_JSON = "2203" 74 | INVALID_DEVICE_STATUS = "2205" 75 | INVALID_COMMAND_ERROR = "2207" 76 | FAIL_DEVICE_CONTROL = "2208" 77 | DEVICE_RESPONSE_DELAY = "2209" 78 | RETRY_REQUEST = "2210" 79 | SYNCING = "2212" 80 | RETRY_AFTER_DELETING_DEVICE = "2213" 81 | FAIL_REQUEST = "2214" 82 | COMMAND_NOT_SUPPORTED_IN_REMOTE_OFF = "2301" 83 | COMMAND_NOT_SUPPORTED_IN_STATE = "2302" 84 | COMMAND_NOT_SUPPORTED_IN_ERROR = "2303" 85 | COMMAND_NOT_SUPPORTED_IN_POWER_OFF = "2304" 86 | COMMAND_NOT_SUPPORTED_IN_MODE = "2305" 87 | 88 | 89 | error_code_mapping = {value: name for name, value in vars(ThinQAPIErrorCodes).items()} 90 | 91 | 92 | class ThinQAPIException(Exception): 93 | """The class that represents an exception for LG ThinQ Connect API.""" 94 | 95 | def __init__(self, code: str, message: str, headers: dict): 96 | """Initialize the exception.""" 97 | self.code = code 98 | self.message = message 99 | self.headers = headers 100 | self.error_name = error_code_mapping.get(code, "UNKNOWN_ERROR") 101 | super().__init__(f"Error: {self.error_name} ({self.code}) - {self.message}") 102 | 103 | def __str__(self) -> str: 104 | return f"ThinQAPIException: {self.error_name} ({self.code}) - {self.message}" 105 | 106 | 107 | class ThinQApi: 108 | """The class for using LG ThinQ Connect API.""" 109 | 110 | def __init__( 111 | self, 112 | session: ClientSession, 113 | access_token: str, 114 | country_code: str, 115 | client_id: str, 116 | mock_response: bool = False, 117 | ): 118 | """Initialize settings.""" 119 | self._access_token = access_token 120 | self._client_id = client_id 121 | self._api_key = API_KEY 122 | self._session = session 123 | self._phase = "OP" 124 | self._country_code = country_code 125 | self._region_code = get_region_from_country(country_code) 126 | self._mock_response = mock_response 127 | 128 | def __await__(self): 129 | yield from self.async_init().__await__() 130 | return self 131 | 132 | async def async_init(self): 133 | pass 134 | 135 | def set_log_level(self, level): 136 | numeric_level = getattr(logging, level.upper(), None) 137 | if not isinstance(numeric_level, int): 138 | raise ValueError(f"Invalid log level: {level}") 139 | _LOGGER.setLevel(numeric_level) 140 | 141 | def _get_url_from_endpoint(self, endpoint: str) -> str: 142 | """Returns the URL to connect from the given endpoint.""" 143 | return f"https://api-{self._region_code.lower()}.lgthinq.com/{endpoint}" 144 | 145 | def _generate_headers(self, headers: dict = {}) -> dict: 146 | """Generate common headers for request.""" 147 | return { 148 | "Authorization": f"Bearer {self._access_token}", 149 | "x-country": self._country_code, 150 | "x-message-id": self._generate_message_id(), 151 | "x-client-id": self._client_id, 152 | "x-api-key": self._api_key, 153 | "x-service-phase": self._phase, 154 | **headers, 155 | } 156 | 157 | async def _async_fetch(self, method: str, url: StrOrURL, **kwargs: Any) -> ClientResponse: 158 | headers: dict[str, Any] = kwargs.pop("headers", {}) 159 | if self._session is None: 160 | self._session = ClientSession() 161 | return await self._session.request( 162 | method=method, 163 | url=url, 164 | **kwargs, 165 | headers=headers, 166 | ) 167 | 168 | async def async_get_device_list(self, timeout: int | float = 15) -> list | None: 169 | return await self.async_request(method=METH_GET, endpoint="devices", timeout=timeout) 170 | 171 | async def async_get_device_profile(self, device_id: str, timeout: int | float = 15) -> dict | None: 172 | return await self.async_request( 173 | method=METH_GET, 174 | endpoint=f"devices/{device_id}/profile", 175 | timeout=timeout, 176 | ) 177 | 178 | async def async_get_device_status(self, device_id: str, timeout: int | float = 15) -> dict | None: 179 | return await self.async_request(method=METH_GET, endpoint=f"devices/{device_id}/state", timeout=timeout) 180 | 181 | async def async_post_device_control(self, device_id: str, payload: Any, timeout: int | float = 15) -> dict | None: 182 | headers = {"x-conditional-control": "true"} 183 | return await self.async_request( 184 | method=METH_POST, 185 | endpoint=f"devices/{device_id}/control", 186 | json=payload, 187 | timeout=timeout, 188 | headers=headers, 189 | ) 190 | 191 | async def async_post_client_register(self, payload: Any, timeout: int | float = 15) -> dict | None: 192 | return await self.async_request( 193 | method=METH_POST, 194 | endpoint="client", 195 | json=payload, 196 | timeout=timeout, 197 | ) 198 | 199 | async def async_delete_client_register(self, payload: Any, timeout: int | float = 15) -> dict | None: 200 | return await self.async_request( 201 | method=METH_DELETE, 202 | endpoint="client", 203 | json=payload, 204 | timeout=timeout, 205 | ) 206 | 207 | async def async_post_client_certificate(self, payload: Any, timeout: int | float = 15) -> dict | None: 208 | return await self.async_request( 209 | method=METH_POST, 210 | endpoint="client/certificate", 211 | json=payload, 212 | timeout=timeout, 213 | ) 214 | 215 | async def async_get_push_list(self, timeout: int | float = 15) -> dict | None: 216 | return await self.async_request( 217 | method=METH_GET, 218 | endpoint="push", 219 | timeout=timeout, 220 | ) 221 | 222 | async def async_post_push_subscribe(self, device_id: str, timeout: int | float = 15) -> dict | None: 223 | return await self.async_request( 224 | method=METH_POST, 225 | endpoint=f"push/{device_id}/subscribe", 226 | timeout=timeout, 227 | ) 228 | 229 | async def async_delete_push_subscribe(self, device_id: str, timeout: int | float = 15) -> dict | None: 230 | return await self.async_request( 231 | method=METH_DELETE, 232 | endpoint=f"push/{device_id}/unsubscribe", 233 | timeout=timeout, 234 | ) 235 | 236 | async def async_get_event_list(self, timeout: int | float = 15) -> dict | None: 237 | return await self.async_request( 238 | method=METH_GET, 239 | endpoint="event", 240 | timeout=timeout, 241 | ) 242 | 243 | async def async_post_event_subscribe(self, device_id: str, timeout: int | float = 15) -> dict | None: 244 | """Subscribe to event notifications for the device.""" 245 | return await self.async_request( 246 | method=METH_POST, 247 | endpoint=f"event/{device_id}/subscribe", 248 | json={"expire": {"unit": "HOUR", "timer": 4464}}, 249 | timeout=timeout, 250 | ) 251 | 252 | async def async_delete_event_subscribe(self, device_id: str, timeout: int | float = 15) -> dict | None: 253 | """Unsubscribe to event notifications for the device.""" 254 | return await self.async_request( 255 | method=METH_DELETE, 256 | endpoint=f"event/{device_id}/unsubscribe", 257 | timeout=timeout, 258 | ) 259 | 260 | async def async_get_push_devices_list(self, timeout: int | float = 15) -> dict | None: 261 | """Get the list of clients subscribed to push notifications for devices registered,unregistered, and alias updated.""" 262 | return await self.async_request( 263 | method=METH_GET, 264 | endpoint="push/devices", 265 | timeout=timeout, 266 | ) 267 | 268 | async def async_post_push_devices_subscribe(self, timeout: int | float = 15) -> dict | None: 269 | """Subscribe to push notifications for devices registered,unregistered, and alias updated.""" 270 | return await self.async_request( 271 | method=METH_POST, 272 | endpoint="push/devices", 273 | timeout=timeout, 274 | ) 275 | 276 | async def async_delete_push_devices_subscribe(self, timeout: int | float = 15) -> dict | None: 277 | """Unsubscribe to push notifications for devices registered,unregistered, and alias updated.""" 278 | return await self.async_request( 279 | method=METH_DELETE, 280 | endpoint="push/devices", 281 | timeout=timeout, 282 | ) 283 | 284 | async def async_get_route(self, timeout: int | float = 15) -> dict | None: 285 | return await self.async_request( 286 | method=METH_GET, 287 | endpoint="route", 288 | timeout=timeout, 289 | ) 290 | 291 | async def async_request(self, method: str, endpoint: str, **kwargs: Any) -> dict | list | None: 292 | url = self._get_url_from_endpoint(endpoint) 293 | headers = self._generate_headers(kwargs.pop("headers", {})) 294 | _LOGGER.debug( 295 | "async_request. method=%s, headers={x-message-id=%s, country=%s}, url=%s", 296 | method, 297 | headers.get("x-message-id"), 298 | headers.get("x-country"), 299 | url, 300 | ) 301 | 302 | if self._mock_response: 303 | return {"message": "Mock Response", "body": kwargs.get("json")} 304 | 305 | async with await self._async_fetch(method=method, url=url, **kwargs, headers=headers) as response: 306 | payload = await response.json() 307 | if response.ok: 308 | return payload.get("response") 309 | else: 310 | raise ThinQAPIException( 311 | code=payload.get("error").get("code", "unknown error code"), 312 | message=payload.get("error").get("message", "unknown error message"), 313 | headers=headers, 314 | ) 315 | 316 | def _generate_message_id(self) -> str: 317 | return base64.urlsafe_b64encode(uuid.uuid4().bytes)[:-2].decode("utf-8") 318 | --------------------------------------------------------------------------------