├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── daikinapi.py ├── example.py ├── pyproject.toml ├── renovate.json ├── requirements.txt ├── setup.cfg ├── setup.py └── tox.ini /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | name: "run tox" 11 | strategy: 12 | matrix: 13 | python-version: [3.8, 3.9, "3.10", 3.11] 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install tox tox-gh-actions 25 | - name: Test with tox 26 | env: 27 | CODECOV_TOKEN: "ec0bfe65-87df-4df5-b794-228c3b32ed96" 28 | run: tox 29 | 30 | package: 31 | name: "Build & verify package" 32 | runs-on: "ubuntu-latest" 33 | needs: [test] 34 | steps: 35 | - uses: "actions/checkout@v4" 36 | - uses: "actions/setup-python@v5" 37 | with: 38 | python-version: "3.11" 39 | 40 | - name: "Install pep517 and twine" 41 | run: "python -m pip install pep517 twine" 42 | - name: "Build package" 43 | run: "python -m pep517.build --source --binary ." 44 | - name: "List result" 45 | run: "ls -l dist" 46 | - name: "Check long_description" 47 | run: "python -m twine check dist/*" 48 | - name: Publish package 49 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 50 | uses: pypa/gh-action-pypi-publish@release/v1 51 | with: 52 | user: __token__ 53 | password: ${{ secrets.PYPI_API_TOKEN }} 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDEs & editors 2 | /.idea/ 3 | 4 | # cache 5 | *.py[cod] 6 | __pycache__/ 7 | 8 | # virtualenv 9 | /virtualenv/ 10 | /venv/ 11 | 12 | # packaging 13 | /*.egg-info/ 14 | /dist/ 15 | /build/ 16 | /.eggs/ 17 | 18 | # testing 19 | /.cache/ 20 | /pytestdebug.log 21 | /.pytest_cache/ 22 | /.tox/ 23 | /.coverage 24 | /htmlcov/ 25 | coverage.xml 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Aarno Aukia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Daikin Airconditioner/Air source heat pump data/control through Wi-Fi API 2 | 3 | [![Python package](https://github.com/arska/python-daikinapi/actions/workflows/main.yml/badge.svg)](https://github.com/arska/python-daikinapi/actions/workflows/main.yml) 4 | [![PyPI version](https://badge.fury.io/py/daikinapi.svg)](https://badge.fury.io/py/daikinapi) 5 | 6 | ## Compatibility 7 | 8 | Tested with Daikin BRP069B41 Wi-Fi Interface and Daikin Emura FTXG-LS indoor unit 9 | 10 | Should be compatible with (https://www.daikin.eu/en_us/product-group/control-systems/daikin-online-controller/connectable-units.html): 11 | * BRP069A41/BRP069B41 12 | * FTXM-M 13 | * CTXM-M 14 | * ATXM-M 15 | * FTXTM-M 16 | * BRP069A45 17 | * FTXG-LS 18 | * FTXG-LW 19 | * FTXJ-MW (built-in) 20 | * FTXJ-MS (built-in) 21 | * BRP069A42/BRP069B42 22 | * FTXZ-N 23 | * FTXS35-42-50K 24 | * FTXS60-71G 25 | * FTX50-60-71GV 26 | * FTXLS-K3 27 | * FTXLS-K3 28 | * FVXS-F 29 | * FLXS-B 30 | * FLXS-B9 31 | * ATXS35-50K 32 | * FVXM-F 33 | * BRP069A43/BRP069B43 34 | * CTXS15-35K 35 | * FTXS20-25K 36 | * FTX20-25-35J3 37 | * FTXL-JV 38 | * ATXS20-25K 39 | * ATX-J3 40 | * ATXL-JV 41 | 42 | Based on exisiting reverse-engineering work: 43 | * https://github.com/ael-code/daikin-control 44 | * https://github.com/ael-code/daikin-aricon-pylib/ 45 | * https://github.com/ael-code/daikin-control/wiki/API-System 46 | 47 | ### Not compatible with 48 | 49 | * FVXM-A 50 | 51 | ## Usage 52 | 53 | see example.py for runnable example 54 | 55 | ```python 56 | from daikinapi import Daikin 57 | 58 | API = Daikin("192.168.1.3") 59 | print(API) 60 | print(API.target_temperature) 61 | ``` 62 | 63 | produces: 64 | ``` 65 | Daikin(host=192.168.1.3,name=mydevice,mac=D0C5D304A0B1) 66 | 21.0 67 | ``` 68 | 69 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Import class from file 3 | """ 4 | 5 | from daikinapi.daikinapi import Daikin 6 | 7 | __all__ = ["Daikin"] 8 | -------------------------------------------------------------------------------- /daikinapi.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python module to get metrics from and control Daikin airconditioners 3 | """ 4 | import datetime 5 | import logging 6 | import urllib.parse 7 | 8 | import requests 9 | 10 | 11 | class Daikin: 12 | """ 13 | Class to get information from Daikin Wireless LAN Connecting Adapter 14 | """ 15 | 16 | _CONTROL_FIELDS = ["f_dir", "f_rate", "mode", "pow", "shum", "stemp"] 17 | """list of fields that need to be defined for a change request""" 18 | 19 | ATTRIBUTES = [ 20 | "power", 21 | "target_temperature", 22 | "target_humidity", 23 | "mode", 24 | "fan_rate", 25 | "fan_direction", 26 | "mac", 27 | "name", 28 | "rev", 29 | "ver", 30 | "type", 31 | "today_runtime", 32 | "today_power_consumption", 33 | "current_month_power_consumption", 34 | "price_int", 35 | "compressor_frequency", 36 | "inside_temperature", 37 | "outside_temperature", 38 | "wifi_settings", 39 | "datetime", 40 | ] 41 | 42 | _host = None 43 | 44 | def __init__(self, host): 45 | """ 46 | Initialize Daikin Aircon API 47 | :param host: host name/IP address to connect to 48 | """ 49 | self._host = host 50 | 51 | def _get(self, path): 52 | """Internal function to connect to and get any information""" 53 | response = requests.get("http://" + self._host + path, timeout=10) 54 | response.raise_for_status() 55 | logging.debug(response.text) 56 | if ( 57 | not len(response.text) > 0 58 | or not response.text[0:4] == "ret=" 59 | or response.text[0:6] == "ret=NG" 60 | ): 61 | return None 62 | fields = {} 63 | for group in response.text.split(","): 64 | element = group.split("=") 65 | if element[0] in ("name", "key"): 66 | fields[element[0]] = urllib.parse.unquote(element[1]) 67 | else: 68 | fields[element[0]] = element[1] 69 | return fields 70 | 71 | def _set(self, path, data): 72 | """Internal function to connect to and update information""" 73 | logging.debug(data) 74 | response = requests.get("http://" + self._host + path, data, timeout=10) 75 | response.raise_for_status() 76 | logging.debug(response.text) 77 | 78 | def _get_basic(self): 79 | """ 80 | Example information: 81 | ret=OK,type=aircon,reg=eu,dst=1,ver=1_2_51,rev=D3A0C9F,pow=1,err=0,location=0, 82 | name=%79%6c%c3%a4%61%75%6c%61,icon=0,method=home only,port=30050,id=,pw=, 83 | lpw_flag=0,adp_kind=3,pv=2,cpv=2,cpv_minor=00,led=1,en_setzone=1, 84 | mac=D0C5D3042E82,adp_mode=run,en_hol=0,grp_name=,en_grp=0 85 | :return: dict 86 | """ 87 | return self._get("/common/basic_info") 88 | 89 | def _get_notify(self): 90 | """ 91 | Example: 92 | ret=OK,auto_off_flg=0,auto_off_tm=- - 93 | :return: dict 94 | """ 95 | return self._get("/common/get_notify") 96 | 97 | def _get_week(self, ex=False): 98 | """ 99 | Example (ex=False): 100 | ret=OK,today_runtime=601,datas=0/0/0/0/0/0/1000 101 | (datas: values in Watts, last day last) 102 | Example (ex=True): 103 | ret=OK,s_dayw=2,week_heat=10/0/0/0/0/0/0/0/0/0/0/0/0/0,week_cool=0/0/0/0/0/0/0/0/0/0/0/0/0/0 104 | (week_*: values in 100Watts, last day first) 105 | :return: dict 106 | """ 107 | return self._get("/aircon/get_week_power" + ("_ex" if ex else "")) 108 | 109 | def _get_year(self, ex=False): 110 | """ 111 | Example (ex=False): 112 | ret=OK,previous_year=0/0/0/0/0/0/0/0/0/0/0/0,this_year=0/0/0/0/0/0/0/0/0/1 113 | (*_year: values in 100Watts per month (jan-dec)) 114 | Example (ex=True): 115 | ret=OK,curr_year_heat=0/0/0/0/0/0/0/0/0/0/0/1, 116 | prev_year_heat=0/0/0/0/0/0/0/0/0/0/0/0, 117 | curr_year_cool=0/0/0/0/0/0/0/0/0/0/0/0, 118 | prev_year_cool=0/0/0/0/0/0/0/0/0/0/0/0 119 | (*_year_*: values in 100Watts per month (jan-dec)) 120 | :return: dict 121 | """ 122 | return self._get("/aircon/get_year_power" + ("_ex" if ex else "")) 123 | 124 | def _get_target(self): 125 | """ 126 | Example: 127 | ret=OK,target=0 128 | :return: dict 129 | """ 130 | return self._get("/aircon/get_target") 131 | 132 | def _get_price(self): 133 | """ 134 | Example: 135 | ret=OK,price_int=27,price_dec=0 136 | :return: dict 137 | """ 138 | return self._get("/aircon/get_price") 139 | 140 | def _get_sensor(self): 141 | """ 142 | Example: 143 | ret=OK,htemp=24.0,hhum=-,otemp=-7.0,err=0,cmpfreq=40 144 | :return: dict 145 | """ 146 | return self._get("/aircon/get_sensor_info") 147 | 148 | def _get_control(self, all_fields=False): 149 | """ 150 | Example: 151 | ret=OK,pow=1,mode=4,adv=,stemp=21.0,shum=0,dt1=25.0,dt2=M,dt3=25.0,dt4=21.0, 152 | dt5=21.0,dt7=25.0,dh1=AUTO,dh2=50,dh3=0,dh4=0,dh5=0,dh7=AUTO,dhh=50,b_mode=4, 153 | b_stemp=21.0,b_shum=0,alert=255,f_rate=A,f_dir=0,b_f_rate=A,b_f_dir=0,dfr1=5, 154 | dfr2=5,dfr3=5,dfr4=A,dfr5=A,dfr6=5,dfr7=5,dfrh=5,dfd1=0,dfd2=0,dfd3=0,dfd4=0, 155 | dfd5=0,dfd6=0,dfd7=0,dfdh=0 156 | :param all_fields: return all fields or just the most relevant f_dir, f_rate, 157 | mode, pow, shum, 158 | stemp 159 | :return: dict 160 | """ 161 | data = self._get("/aircon/get_control_info") 162 | if all_fields: 163 | return data 164 | return {key: data[key] for key in self._CONTROL_FIELDS} 165 | 166 | def _get_model(self): 167 | """ 168 | Example: 169 | ret=OK,model=0ABB,type=N,pv=2,cpv=2,cpv_minor=00,mid=NA,humd=0,s_humd=0, 170 | acled=0,land=0,elec=0,temp=1,temp_rng=0,m_dtct=1,ac_dst=--,disp_dry=0,dmnd=0, 171 | en_scdltmr=1,en_frate=1,en_fdir=1,s_fdir=3,en_rtemp_a=0,en_spmode=0, 172 | en_ipw_sep=0,en_mompow=0 173 | :return: dict 174 | """ 175 | return self._get("/aircon/get_model_info") 176 | 177 | def _get_remote(self): 178 | """ 179 | Example: 180 | ret=OK,method=home only,notice_ip_int=3600,notice_sync_int=60 181 | :return: dict 182 | """ 183 | return self._get("/common/get_remote_method") 184 | 185 | def _get_wifi(self): 186 | """ 187 | Example: 188 | ret=OK,ssid=wireless_ssid,security=mixed,key=%77%69%66%69%6b%65%79,link=1 189 | :return: dict 190 | """ 191 | return self._get("/common/get_wifi_setting") 192 | 193 | def _get_datetime(self): 194 | """ 195 | Example: 196 | ret=OK,sta=1,cur=2022/12/01 22:01:02,reg=eu,dst=1,zone=10 197 | :return: dict 198 | """ 199 | return self._get("/common/get_datetime") 200 | 201 | def _set_datetime(self, date_time=None): 202 | """ 203 | Example: 204 | ret=OK 205 | :return: None 206 | """ 207 | if date_time is None: 208 | date_time = datetime.datetime.now() 209 | date_time = date_time.astimezone(tz=datetime.timezone.utc) 210 | data = { 211 | 'lpw': '', 212 | 'date': f'{date_time.year:d}/{date_time.month:d}/{date_time.day:d}', 213 | 'zone': 'GMT', 214 | 'time': f'{date_time.hour:d}:{date_time.minute:d}:{date_time.second:d}', 215 | } 216 | data = urllib.parse.urlencode(data) 217 | return self._set("/common/notify_date_time", data) 218 | 219 | def _do_reboot(self): 220 | return self._get("/common/reboot") 221 | 222 | def _set_wifi(self, ssid, key, do_reboot=True): 223 | """ 224 | Set the wifi settings 225 | :param ssid: ssid of the new network 226 | :param key: key of the new network 227 | :param do_reboot: boolean indicating whether to reboot, to activate the settings 228 | :return: None 229 | """ 230 | key_encoded = "".join("%" + hex(ord(c))[2:].rjust(2, "0") for c in key) 231 | data = {"ssid": ssid, "key": key_encoded, "security": "mixed"} 232 | self._set("/common/set_wifi_setting", data) 233 | if do_reboot: 234 | res = self._do_reboot() 235 | logging.debug("Reboot ordered to activate wifi changes: %s", res) 236 | 237 | @property 238 | def power(self): 239 | """ 240 | unit on/off 241 | :return: "1" for ON, "0" for OFF 242 | """ 243 | return int(self._get_control()["pow"]) 244 | 245 | @property 246 | def target_temperature(self): 247 | """ 248 | target temperature 249 | range of accepted values determined by mode: AUTO:18-31, HOT:10-31, COLD:18-33 250 | NOTE: when switched to fan-mode(7) the reported temperature target becomes '--' 251 | when switched to drying-mode(2) the reported temperature target becomes 'M' 252 | :return: string containing target temperature or '--' or 'M' 253 | """ 254 | return self._get_control()["stemp"] 255 | 256 | @property 257 | def target_humidity(self): 258 | """ 259 | target humidity 260 | :return: 0 261 | """ 262 | return float(self._get_control()["shum"]) 263 | 264 | @property 265 | def mode(self): 266 | """ 267 | operation mode 268 | :return: "0": "AUTO", "1": "AUTO", "2": "DEHUMIDIFICATOR", "3": "COLD", 269 | "4": "HOT", "6": "FAN", "7": "AUTO" 270 | """ 271 | return int(self._get_control()["mode"]) 272 | 273 | @property 274 | def fan_rate(self): 275 | """ 276 | fan speed 277 | :return: "A":"auto", "B":"silence", "3":"fan level 1","4":"fan level 2", 278 | "5":"fan level 3", "6":"fan level 4","7":"fan level 5" 279 | """ 280 | return self._get_control()["f_rate"] 281 | 282 | @property 283 | def fan_direction(self): 284 | """ 285 | horizontal/vertical fan wings motion 286 | :return: "0":"all wings stopped", "1":"vertical wings motion", 287 | "2":"horizontal wings motion", "3":"vertical and horizontal wings motion" 288 | """ 289 | return int(self._get_control()["f_dir"]) 290 | 291 | @property 292 | def wifi_settings(self): 293 | """ 294 | wifi settings 295 | :return: tuple containing ssid and key of wifi network 296 | """ 297 | wifi = self._get_wifi() 298 | return wifi["ssid"], wifi["key"] 299 | 300 | @property 301 | def datetime(self): 302 | """ 303 | datetime on the device 304 | :return: string of datetime on the device (yyyy/mm/dd HH:MM:SS), 305 | or None if not retrievable 306 | """ 307 | date_time = self._get_datetime()["cur"] 308 | return date_time if date_time != "-" else None 309 | 310 | @power.setter 311 | def power(self, value): 312 | self._control_set("pow", value) 313 | 314 | @target_temperature.setter 315 | def target_temperature(self, value): 316 | self._control_set("stemp", value) 317 | 318 | @target_humidity.setter 319 | def target_humidity(self, value): 320 | self._control_set("shum", value) 321 | 322 | @mode.setter 323 | def mode(self, value): 324 | self._control_set("mode", value) 325 | 326 | @fan_rate.setter 327 | def fan_rate(self, value): 328 | self._control_set("f_rate", value) 329 | 330 | @fan_direction.setter 331 | def fan_direction(self, value): 332 | self._control_set("f_dir", value) 333 | 334 | @wifi_settings.setter 335 | def wifi_settings(self, value): 336 | ssid, key = value 337 | self._set_wifi(ssid, key) 338 | 339 | @datetime.setter 340 | def datetime(self, value): 341 | self._set_datetime(date_time=value) 342 | 343 | def _control_set(self, key, value): 344 | """ 345 | set a get_control() item via one of the property.setters 346 | 347 | will fetch the current settings to change this one value, so this is not safe 348 | against concurrent changes 349 | :param key: item name e.g. "pow" 350 | :param value: set to value e.g. 1, "1" or "ON" 351 | :return: None 352 | """ 353 | data = self._get_control() 354 | data[key] = value 355 | self._set("/aircon/set_control_info", data) 356 | 357 | @property 358 | def mac(self): 359 | """ 360 | wifi module mac address 361 | :return: A0B1C2D3E4F5G6 formatted mac address 362 | """ 363 | return self._get_basic()["mac"] 364 | 365 | @property 366 | def name(self): 367 | """ 368 | user defined unit name 369 | :return: string 370 | """ 371 | return self._get_basic()["name"] 372 | 373 | @property 374 | def rev(self): 375 | """ 376 | hardware revision 377 | :return: e.g. D3A0C9F 378 | """ 379 | return self._get_basic()["rev"] 380 | 381 | @property 382 | def ver(self): 383 | """ 384 | wifi module software version 385 | :return: e.g. 1_2_51 386 | """ 387 | return self._get_basic()["ver"] 388 | 389 | @property 390 | def type(self): 391 | """ 392 | unit type 393 | :return: e.g. "aircon" 394 | """ 395 | return self._get_basic()["type"] 396 | 397 | @property 398 | def today_runtime(self): 399 | """ 400 | unit run time today 401 | :return: minutes of runtime 402 | """ 403 | return int(self._get_week()["today_runtime"]) 404 | 405 | def _today_power_consumption_ex(self, ex=True, mode="heat"): 406 | """ 407 | unit power consumption today (in Watts) 408 | :param ex: boolean indicating whether to take form '_ex' 409 | :param mode: string from ("heat", "cool") describing mode of operation; 410 | ignored if ex==False 411 | :return: Watts of power consumption 412 | """ 413 | assert not ex or mode in ( 414 | "heat", 415 | "cool", 416 | ), 'mode should be from ("heat", "cool") if ex==True' 417 | res = self._get_week(ex=ex) 418 | if res is None: 419 | return None 420 | res = int(res[f"week_{mode}" if ex else "datas"].split("/")[0 if ex else -1]) 421 | return res * 100 if ex else res 422 | 423 | @property 424 | def today_power_consumption(self): 425 | """ 426 | unit power consumption today (in Watts) 427 | :return: Watts of power consumption 428 | """ 429 | return self._today_power_consumption_ex(ex=False, mode=None) 430 | 431 | def _month_power_consumption(self, month=None): 432 | """ 433 | energy consumption 434 | :param month: request a particular month-of-year (january=1); 435 | None defaults to current month 436 | :return: current-of-year energy consumption in kWh or None if not retrievable 437 | """ 438 | if month is None: 439 | if self.datetime is None: 440 | return None 441 | month = int(self.datetime.split("/")[1]) 442 | return int(self._get_year()["this_year"].split("/")[month - 1]) / 10.0 443 | 444 | @property 445 | def current_month_power_consumption(self): 446 | """ 447 | energy consumption 448 | :return: current month to date energy consumption in kWh or None if not retrievable 449 | """ 450 | return self._month_power_consumption() 451 | 452 | @property 453 | def price_int(self): 454 | """ 455 | ? 456 | :return: ? 457 | """ 458 | return int(self._get_price()["price_int"]) 459 | 460 | @property 461 | def compressor_frequency(self): 462 | """ 463 | compressor frequency/power 464 | :return: The outside compressor load, normally between 12 to 84 max 465 | """ 466 | return int(self._get_sensor()["cmpfreq"]) 467 | 468 | @property 469 | def inside_temperature(self): 470 | """ 471 | inside current temperature 472 | :return: degrees centigrade 473 | """ 474 | return float(self._get_sensor()["htemp"]) 475 | 476 | @property 477 | def inside_humidity(self): 478 | """ 479 | inside relative humidity or '-' if no humidity sensor is installed 480 | :return: string containing relative humidity or '-' 481 | """ 482 | return self._get_sensor()["hhum"] 483 | 484 | @property 485 | def outside_temperature(self): 486 | """ 487 | outside current temperature 488 | :return: degrees centigrade 489 | """ 490 | return float(self._get_sensor()["otemp"]) 491 | 492 | def _get_all(self): 493 | """ 494 | Get and aggregate all data endpoints 495 | :return: dict of all aircon parameters 496 | """ 497 | fields = {} 498 | fields.update(self._get_basic()) 499 | fields.update(self._get_notify()) 500 | fields.update(self._get_week()) 501 | fields.update(self._get_year()) 502 | fields.update(self._get_target()) 503 | fields.update(self._get_price()) 504 | fields.update(self._get_sensor()) 505 | fields.update(self._get_control()) 506 | fields.update(self._get_model()) 507 | fields.update(self._get_remote()) 508 | fields.update(self._get_wifi()) 509 | fields.update(self._get_datetime()) 510 | return fields 511 | 512 | def __str__(self): 513 | return f"Daikin(host={self._host},name={self.name},mac={self.mac})" 514 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example usage of daikinapi module 3 | 4 | use e.g. with "python example.py 192.168.1.3" 5 | """ 6 | import argparse 7 | import logging 8 | 9 | from daikinapi import Daikin 10 | 11 | PARSER = argparse.ArgumentParser( 12 | description="Get metrics from Daikin airconditioning wifi module" 13 | ) 14 | PARSER.add_argument( 15 | "-v", "--verbose", help="set logging to debug", action="store_true", default=False, 16 | ) 17 | PARSER.add_argument("hosts", help="list of airconditioning units to query", nargs="*") 18 | ARGS = PARSER.parse_args() 19 | 20 | LOGFORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 21 | 22 | if ARGS.verbose: 23 | logging.basicConfig(level=logging.DEBUG, format=LOGFORMAT) 24 | else: 25 | logging.basicConfig(level=logging.INFO, format=LOGFORMAT) 26 | logging.getLogger("requests.packages.urllib3.connectionpool").setLevel( 27 | logging.WARNING 28 | ) 29 | 30 | logging.debug("starting with arguments: %s", ARGS) 31 | 32 | for host in ARGS.hosts: 33 | API = Daikin(host) 34 | print(API) 35 | for attribute in API.ATTRIBUTES: 36 | print(attribute, getattr(API, attribute)) 37 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools >= 40.6.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | ":assignee(arska)" 5 | ], 6 | "packageRules": [ 7 | { 8 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"], 9 | "automerge": true 10 | } 11 | ] 12 | 13 | } 14 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arska/python-daikinapi/00bbb972ff2237ff7a8057020dda0520c6b27f95/requirements.txt -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # This flag says to generate wheels that support both Python 2 and Python 3 | # 3. If your code will not run unchanged on both Python 2 and 3, you will 4 | # need to generate separate wheels for each Python version that you 5 | # support. 6 | universal=1 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | daikinapi python module manifest 3 | """ 4 | from os.path import abspath, dirname, join 5 | from setuptools import setup 6 | 7 | 8 | def read_file(filename): 9 | """Get the contents of a file""" 10 | here = abspath(dirname(__file__)) 11 | with open(join(here, filename), encoding="utf-8") as file: 12 | return file.read() 13 | 14 | 15 | setup( 16 | name="daikinapi", 17 | version_config={"dirty_template": "{tag}"}, 18 | description="Get metrics from Daikin airconditioning unit wifi module", 19 | long_description=read_file("README.md"), 20 | long_description_content_type="text/markdown", 21 | packages=["daikinapi"], 22 | package_dir={"daikinapi": "."}, 23 | keywords=["Daikin", "airconditioning", "API"], 24 | classifiers=[ 25 | "Intended Audience :: Developers", 26 | "License :: OSI Approved :: MIT License", 27 | "Programming Language :: Python :: 3.6", 28 | "Programming Language :: Python :: 3.7", 29 | "Programming Language :: Python :: 3.8", 30 | "Programming Language :: Python :: 3.9", 31 | ], 32 | url="https://github.com/arska/python-daikinapi", 33 | author="Aarno Aukia", 34 | author_email="aarno@aukia.com", 35 | license="MIT", 36 | python_requires=">=3.6", 37 | extras_require={"dev": ["tox"]}, 38 | install_requires=["requests>=2", "urllib3>=1.24"], 39 | setup_requires=["setuptools-git-versioning"], 40 | ) 41 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | clean 4 | pylint 5 | flake8 6 | py38 7 | py39 8 | py310 9 | py311 10 | report 11 | skip_missing_interpreters = True 12 | #requires = 13 | # tox-pip-extensions 14 | basepython = 15 | py38: python3.8 16 | py39: python3.9 17 | py310: python3.10 18 | py311: python3.11 19 | 20 | [gh-actions] 21 | python = 22 | 3.8: py38 23 | 3.9: py39 24 | "3.10": py310 25 | 3.11: py311, clean, pylint, flake8, report 26 | 27 | [testenv] 28 | #deps = 29 | # pytest 30 | # pytest-cov 31 | #commands = 32 | # pip install -e . 33 | # pytest --cov --cov-append 34 | #norecursedirs = .tox 35 | 36 | [testenv:clean] 37 | basepython = python3.11 38 | #deps = coverage 39 | #skip_install = true 40 | #commands = coverage erase 41 | 42 | [testenv:report] 43 | basepython = python3.11 44 | #passenv = TOXENV CI TRAVIS TRAVIS_* CODECOV_* 45 | #deps = 46 | # coverage 47 | # codecov 48 | #skip_install = true 49 | #commands = 50 | # coverage report --omit='.tox/*' 51 | # coverage html --omit='.tox/*' 52 | # codecov -e TOXENV 53 | 54 | [testenv:flake8] 55 | basepython = python3.11 56 | deps = flake8 57 | flake8-isort 58 | flake8-black 59 | flake8-blind-except 60 | flake8-builtins 61 | flake8-docstrings 62 | flake8-bugbear 63 | flake8-mypy 64 | pep8-naming 65 | flake8-assertive 66 | #flake8-mock 67 | #flake8-bandit 68 | commands = flake8 69 | 70 | [testenv:pylint] 71 | basepython = python3.11 72 | deps = 73 | pylint 74 | -rrequirements.txt 75 | commands = pylint --disable=R0904 daikinapi 76 | # ignore too many >20 public methods since they are all attributes 77 | 78 | [flake8] 79 | exclude = .tox,venv,*.egg*,.git,__pycache__,*.pyc*,build,dist 80 | max-line-length = 88 81 | select = C,E,F,G,W,B,B902,B950 82 | ignore = E501,W503,BLK100 83 | --------------------------------------------------------------------------------