diff --git a/README.md b/README.md new file mode 100644 index 0000000..ad704ab --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# check_air_purifier + +check_air_purifier.py is a python 3 script for monitoring air purifiers made by philips. Communication with the device is made with a customized version of py-air-control. Tested with AC1214_10, but should work with any other philips device. + +Features: + +* Monitor air-quality (allergen index / pm2.5) and filter status including thresholds and perfdata +* Check device status (fan speed, power, light, updates, network) + + +## Installation + +Use the package manager [pip3](https://pip.pypa.io/en/stable/) to install the required module py-air-control (tested with version 0.5.0). + +```bash +pip3 install py-air-control +``` + +Put the plugin into libexec and extend your checkcommands. For icinga2 you can use check_air_purifier.cfg. + +## Usage + +``` +usage: check_air_purifier.py [-h] -H HOSTNAME -m + {deviceinfo,filters,airquality} [-w WARNING] + [-c CRITICAL] + +optional arguments: + -h, --help show this help message and exit + -H HOSTNAME, --hostname HOSTNAME + Hostname / IP of air purifier + -m {deviceinfo,filters,airquality}, --mode {deviceinfo,filters,airquality} + mode to check + -w WARNING, --warning WARNING + Warning threshold + -c CRITICAL, --critical CRITICAL + Critical threshold +``` + +## Examples + +```bash +check_air_purifier.py -H 192.168.10.120 -m 'deviceinfo' +OK: Power is ON - Mode is auto - Fan Speed is 2 - Light brightness is 50 - Button Light is ON - Used Index is IAI - Child lock is False - name is AC1214_10 - version is 2 - upgrade is - state is idle - progress is 0 - statusmsg is - mandatory is False - ssid is myssid - password is mypassword - protection is wpa-2 - ipaddress is 192.168.10.120 - netmask is 255.255.255.0 - gateway is 192.168.10.1 - dhcp is True - macaddress is mymacaddress - cppid is mycppid|'Fan Speed'=2 'Light brightness'=50 + +check_air_purifier.py -H 192.168.10.120 -m 'filters' --warning 16 --critical 8 +OK: Pre-filter and Wick is ok (44 hours remaining) - Active carbon filter is ok (2084 hours remaining) - HEPA filter is ok (4484 hours remaining)|'Pre-filter and Wick'=44 'Active carbon filter'=2084 'HEPA filter'=4484 + +check_air_purifier.py -H 192.168.10.120 -m 'airquality' --warning 8 --critical 10 +OK: Allergen index is ok (4) - PM25 is 19|'Allergen index'=4 'PM25'=19 +``` + +## Contributing +Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. + +## License +[MIT](https://choosealicense.com/licenses/mit/) diff --git a/check_air_purifier.cfg b/check_air_purifier.cfg new file mode 100644 index 0000000..7ec2827 --- /dev/null +++ b/check_air_purifier.cfg @@ -0,0 +1,25 @@ +object CheckCommand "my-air-purifier" { + import "ipv4-or-ipv6" + command = [ PluginContribDir + "/check_air_purifier.py" ] + + arguments = { + "--hostname" = { + value = "$air_purifier_address$" + description = "the air purifiers hostname" + } + "--warning" = { + value = "$air_purifier_warning$" + description = "the warning threshold" + } + "--critical" = { + value = "$air_purifier_critical$" + description = "the critical threshold" + } + "--mode" = { + value = "$air_purifier_mode$" + description = "the mode of the plugin" + } + } + vars.air_purifier_address = "$check_address$" +} + diff --git a/check_air_purifier.py b/check_air_purifier.py new file mode 100755 index 0000000..4d6d7b3 --- /dev/null +++ b/check_air_purifier.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +import sys +import argparse +from airctrl import airctrl as air +import pprint + +class AirClient(air.AirClient): + + def get_status(self, debug=False): + url = 'http://{}/di/v1/products/1/air'.format(self._host) + status = self._get(url) + return self._dump_status(status, debug=debug) + + def get_wifi(self): + url = 'http://{}/di/v1/products/0/wifi'.format(self._host) + wifi = self._get(url) + return wifi + + def get_firmware(self): + url = 'http://{}/di/v1/products/0/firmware'.format(self._host) + firmware = self._get(url) + return firmware + + def get_filters(self): + values = {} + + url = 'http://{}/di/v1/products/1/fltsts'.format(self._host) + filters = self._get(url) + #print('Pre-filter and Wick: clean in {} hours'.format(filters['fltsts0'])) + values['Pre-filter and Wick'] = filters['fltsts0'] + if 'wicksts' in filters: + values['Wick filter'] = filters['wicksts'] + # print('Wick filter: replace in {} hours'.format(filters['wicksts'])) + values['Active carbon filter'] = filters['fltsts2'] + values['HEPA filter'] = filters['fltsts1'] + #print('Active carbon filter: replace in {} hours'.format(filters['fltsts2'])) + #print('HEPA filter: replace in {} hours'.format(filters['fltsts1'])) + + return values + + + def _dump_status(self, status, debug=False): + values = {} + + if debug: + pprint.pprint(status) + pprint() + if 'pwr' in status: + pwr = status['pwr'] + pwr_str = {'1': 'ON', '0': 'OFF'} + pwr = pwr_str.get(pwr, pwr) + values['Power'] = pwr + #print('[pwr] Power: {}'.format(pwr)) + if 'pm25' in status: + pm25 = status['pm25'] + values['PM25'] = pm25 + #print('[pm25] PM25: {}'.format(pm25)) + if 'rh' in status: + rh = status['rh'] + values['Humidity'] = rh + #print('[rh] Humidity: {}'.format(rh)) + if 'rhset' in status: + rhset = status['rhset'] + values['Target humidity'] = rhset + #print('[rhset] Target humidity: {}'.format(rhset)) + if 'iaql' in status: + iaql = status['iaql'] + values['Allergen index'] = iaql + #print('[iaql] Allergen index: {}'.format(iaql)) + if 'temp' in status: + temp = status['temp'] + values['Temperature'] = temp + #print('[temp] Temperature: {}'.format(temp)) + if 'func' in status: + func = status['func'] + func_str = {'P': 'Purification', 'PH': 'Purification & Humidification'} + func = func_str.get(func, func) + values['Function'] = func + #print('[func] Function: {}'.format(func)) + if 'mode' in status: + mode = status['mode'] + mode_str = {'P': 'auto', 'A': 'allergen', 'S': 'sleep', 'M': 'manual', 'B': 'bacteria', 'N': 'night'} + mode = mode_str.get(mode, mode) + values['Mode'] = mode + #print('[mode] Mode: {}'.format(mode)) + if 'om' in status: + om = status['om'] + om_str = {'s': 'silent', 't': 'turbo'} + om = om_str.get(om, om) + values['Fan Speed'] = om + #print('[om] Fan speed: {}'.format(om)) + if 'aqil' in status: + aqil = status['aqil'] + values['Light brightness'] = aqil + #print('[aqil] Light brightness: {}'.format(aqil)) + if 'uil' in status: + uil = status['uil'] + uil_str = {'1': 'ON', '0': 'OFF'} + uil = uil_str.get(uil, uil) + values['Button Light'] = uil + #print('[uil] Buttons light: {}'.format(uil)) + if 'ddp' in status: + ddp = status['ddp'] + ddp_str = {'1': 'PM2.5', '0': 'IAI'} + ddp = ddp_str.get(ddp, ddp) + values['Used Index'] = ddp + #print('[ddp] Used index: {}'.format(ddp)) + if 'wl' in status: + wl = status['wl'] + values['Water level'] = wl + #print('[wl] Water level: {}'.format(wl)) + if 'cl' in status: + cl = status['cl'] + values['Child lock'] = cl + #print('[cl] Child lock: {}'.format(cl)) + if 'dt' in status: + dt = status['dt'] + if dt != 0: + values['Timer'] = dt + #print('[dt] Timer: {} hours'.format(dt)) + if 'dtrs' in status: + dtrs = status['dtrs'] + if dtrs != 0: + values['Times minutes'] = dtrs + #print('[dtrs] Timer: {} minutes left'.format(dtrs)) + if 'err' in status: + err = status['err'] + if err != 0: + err_str = {49408: 'no water', 32768: 'water tank open'} + err = err_str.get(err, err) + values['Error'] = err + #print('-'*20) + #print('Error: {}'.format(err)) + return values + +if __name__ == '__main__': + + parser = argparse.ArgumentParser() + parser.add_argument('-H', '--hostname', required=True, type=str, + help='Hostname / IP of air purifier') + parser.add_argument('-m', '--mode', required=True, choices=['deviceinfo','filters','airquality'], type=str, + help='mode to check') + parser.add_argument('-w', '--warning', type=float, + help='Warning threshold') + parser.add_argument('-c', '--critical', type=float, + help='Critical threshold') + args = parser.parse_args() + + mode = args.mode + warning = args.warning + critical = args.critical + + message = '' + perfdata = '' + RC = [0] + RCStatus = ('OK','WARNING','CRITICAL','UNKNOWN') + + c = AirClient(args.hostname) + c.load_key() + + if mode == 'deviceinfo': + status = c.get_status() + wifi = c.get_wifi() + firmware = c.get_firmware() + + #message += 'Power: {}\n'.format(status['Power']) + + for item,value in status.items(): + if item in ['Button Light','Child lock','Fan Speed','Light brightness','Mode','Used Index','Power','Humidity','Target humidity','Temperature','Function','Water level']: + message += '{} is {} - '.format(item,value) + + if item in ['Fan Speed','Light brightness','Humidity','Target humidity','Temperature','Water level']: + perfdata += "'{}'={} ".format(item,value) + + for item,value in firmware.items(): + message += '{} is {} - '.format(item,value) + for item,value in wifi.items(): + message += '{} is {} - '.format(item,value) + + if mode == 'filters': + filters = c.get_filters() + for filter,hours in filters.items(): + if hours <= critical: + message += '{} is critical ({} hours remaining) - '.format(filter,hours) + RC.append(2) + elif hours <= warning: + message += '{} is warning ({} hours remaining) - '.format(filter,hours) + RC.append(1) + else: + message += '{} is ok ({} hours remaining) - '.format(filter,hours) + perfdata += "'{}'={} ".format(filter,hours) + + if mode == 'airquality': + airquality = c.get_status() + allergenindex = airquality['Allergen index'] + pm25 = airquality['PM25'] + if allergenindex >= critical: + message += 'Allergen index is critical ({}) - '.format(allergenindex) + RC.append(2) + elif allergenindex >= warning: + message += 'Allergen index is warning ({}) - '.format(allergenindex) + RC.append(1) + else: + message += 'Allergen index is ok ({}) - '.format(allergenindex) + RC.append(0) + message += 'PM25 is {} - '.format(pm25) + perfdata += "'Allergen index'={} ".format(allergenindex) + perfdata += "'PM25'={} ".format(pm25) + + print('{}: {}|{}'.format(RCStatus[max(RC)],message[:-3],perfdata)) + sys.exit(max(RC))