Repository
Munin (contrib)
Last change
2021-04-06
Graph Categories
Capabilities
Keywords
Language
Python (3.x)
License
GPL-2.0-only
Authors

hue

Name

hue - monitor various data from Philips Hue devices

Applicable Systems

Homes with Philips Hue light bulbs and motion sensors.

Configuration

Following config is needed:

[hue]
env.host hue-bridge-ip-or-hostname
env.auth auth-key

Author

Kim B. Heino b@bbbs.net

License

GPLv2

#!/usr/bin/env python3

"""Munin plugin to read various data from Philips Hue devices.

=head1 NAME

hue - monitor various data from Philips Hue devices

=head1 APPLICABLE SYSTEMS

Homes with Philips Hue light bulbs and motion sensors.

=head1 CONFIGURATION

Following config is needed:

    [hue]
    env.host hue-bridge-ip-or-hostname
    env.auth auth-key

=head1 AUTHOR

Kim B. Heino <b@bbbs.net>

=head1 LICENSE

GPLv2
=cut
"""

import json
import os
import sys
import unicodedata
import urllib.request


def safename(name):
    """Return safe variable name."""
    # Convert ä->a as isalpha('ä') is true
    value = unicodedata.normalize('NFKD', name)
    value = value.encode('ASCII', 'ignore').decode('utf-8')

    # Remove non-alphanumeric chars
    return ''.join(char.lower() if char.isalnum() else '_' for char in value)


def sensor_name(data, sensor):
    """Find "parent" sensor for temperatue/light."""
    uniqueid = sensor['uniqueid'][:27]
    name = sensor['name']
    for item in data['sensors'].values():
        if item.get('uniqueid', '').startswith(uniqueid):
            name = item['name']
            break
    return name


def print_temperatures(data, config, both):
    """Motion sensor has temperature sensor - Read it's value."""
    if not any([sensor['type'] == 'ZLLTemperature'
                for sensor in data['sensors'].values()]):
        return

    print('multigraph hue_temp')
    if config:
        print('graph_title Hue temperatures')
        print('graph_vlabel Celsius')
        print('graph_category sensors')
        print('graph_args --base 1000')
        print('graph_info Temperature sensor values.')
    for sensor in data['sensors'].values():
        if sensor['type'] != 'ZLLTemperature':
            continue
        label = safename(sensor['name'])
        if config:
            print('{}.label {}'.format(label, sensor_name(data, sensor)))
        if not config or both:
            value = sensor['state']['temperature'] / 100
            print('{}.value {}'.format(label, value))


def print_light_levels(data, config, both):
    """Motion sensor has light level sensor - Read it's value."""
    if not any([sensor['type'] == 'ZLLLightLevel'
                for sensor in data['sensors'].values()]):
        return

    print('multigraph hue_light_level')
    if config:
        print('graph_title Hue light levels')
        print('graph_vlabel Lux')
        print('graph_category sensors')
        print('graph_args --base 1000')
        print('graph_scale no')
        print('graph_info Light level sensor values.')
    for sensor in data['sensors'].values():
        if sensor['type'] != 'ZLLLightLevel':
            continue
        label = safename(sensor['name'])
        if config:
            print('{}.label {}'.format(label, sensor_name(data, sensor)))
        if not config or both:
            value = 10 ** ((sensor['state']['lightlevel'] - 1) / 10000)
            print('{}.value {}'.format(label, value))


def print_lights(data, config, both):
    """Light bulbs on/off."""
    if not data['lights']:
        return

    print('multigraph hue_lights')
    if config:
        print('graph_title Hue lights on')
        print('graph_vlabel Count')
        print('graph_category sensors')
        print('graph_args --base 1000 --lower-limit 0')
        print('graph_info Number of turned on lights.')
    count = 0
    for light in data['lights'].values():
        if light['state']['on']:
            count += 1
    if config:
        print('lights.label Number of lights on')
    if not config or both:
        print('lights.value {}'.format(count))


def print_data(config):
    """Print config or values."""
    # Get values
    url = 'http://{}/api/{}/'.format(os.getenv('host'), os.getenv('auth'))
    try:
        data = json.loads(urllib.request.urlopen(url, timeout=50).read())
    except (OSError, ValueError, TypeError):
        return
    both = os.getenv('MUNIN_CAP_DIRTYCONFIG') == '1'

    # Print config/values
    print_temperatures(data, config, both)
    print_light_levels(data, config, both)
    print_lights(data, config, both)


if __name__ == '__main__':
    if len(sys.argv) > 1 and sys.argv[1] == 'autoconf':
        print('no (this is not autoconf plugin)')
    elif len(sys.argv) > 1 and sys.argv[1] == 'config':
        print_data(True)
    else:
        print_data(False)