Unifi sensor custom component

My bad… should be - key: sensor.unifi.ap*. Anyway… you got it working :-).

1 Like

Actually, it works without the key:
Maybe I’d only need that if I wanted to give the attribute a specific name - will play with it when I have some time,

I have wired devices that show up as connected to my PoE switch and not an AP, is that what you mean?

yes! I didn’t thought about that as I don’t have other “wired” devices! I put a python script here that will print your devices information in json. At the bottom of the file you should edit your user/pass/ip at the line “api = API(username=“admin”, password=“your_password”, baseurl=“https://your_ip:8443”, verify_ssl=False)”.

You may run it with “python3 unifi_helper.py” or “python3 unifi_helper.py > output.txt” to save the output to a file. If you share the output (edit/change sensitive information, of course) I should be able to correct my code.

output.yaml (78.1 KB)
Here is the output. It’s a txt file, but those don’t seem to be allowed, so I changed the extension to .yaml. I hope that will work.
Thanks for looking at this, If I can’t contribute code yet, I’m more than happy to break things :slight_smile:

Thanks,

Denny

Thanks! I should have a new version tomorrow.

One more thing… How should be the better way to present information about wired devices? From your image it seems that they use the “network” attribute (LAN or Cameras in your case) or we may just list them as “wired”.

Well LAN or Camera refer to the VLANs that they are connected to. I could see how that would be nice to use, but I don’t see why just Wired wouldn’t work.

You rock, thanks!

Hi,

New version o github. May you try, plz?

It works! Very awesome. Thanks for doing this.

Hi @clyra,

I’m using your customer sensor and don’t work.

I have this error in my logs:

File "/config/custom_components/sensor/unifi.py", line 261, in <module>
    self.logout()
NameError: name 'self' is not defined
2018-09-22 11:45:44 ERROR (MainThread) [homeassistant.core] Error doing job: Task exception was never retrieved

My configuration is this one:

- platform: unifi
  name: unifi
  region: default
  username: !secret unifi_user
  password: !secret unifi_pass
  url: https://192.168.1.100:8443

I’m using HA version 0.77.3.

And this is the python code that i’m using:

"""
Unifi sensor. Shows the total number os devices connected. Also shows the number of devices per
AP and per essid as attributes.
with code from https://github.com/frehov/Unifi-Python-API
Version 0.2
"""

from datetime import timedelta
from requests import Session
import json
import re
from typing import Pattern, Dict, Union
import logging

import voluptuous as vol

from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
    CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_REGION,
    CONF_URL, CONF_VERIFY_SSL, STATE_UNKNOWN, PRECISION_WHOLE)
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import template


REQUIREMENTS = []

_LOGGER = logging.getLogger(__name__)


DOMAIN = 'sensor'
ENTITY_ID_FORMAT = DOMAIN + '.{}'

DEFAULT_NAME = 'Unifi'
DEFAULT_SITE  = 'default'
DEFAULT_VERIFYSSL = False

SCAN_INTERVAL = timedelta(seconds=60)

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    vol.Required(CONF_USERNAME): cv.string,
    vol.Required(CONF_PASSWORD): cv.string,
    vol.Required(CONF_URL): cv.url,
    vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
    vol.Optional(CONF_REGION, default=DEFAULT_SITE): cv.string,
    vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFYSSL): cv.boolean
})


def setup_platform(hass, config, add_devices, discovery_info=None):
    """Set up the Unifi Sensor."""
    name = config.get(CONF_NAME)
    username = config.get(CONF_USERNAME)
    password = config.get(CONF_PASSWORD)
    baseurl = config.get(CONF_URL)
    site = config.get(CONF_REGION)
    verify_ssl = config.get(CONF_VERIFY_SSL)

    data = UnifiSensorData(hass, username, password, site, baseurl, verify_ssl)

    add_devices([UnifiSensor(hass, data, name)], True)


class UnifiSensor(Entity):
    """Representation of a Unifi Sensor."""

    def __init__(self, hass, data, name):
        """Initialize the sensor."""
        self._hass = hass
        self._data = data
        self._name= name
        self._state = None
        self._attributes = None
        self._unit_of_measurement = 'devices'

    @property
    def name(self):
        """Return the name of the sensor."""
        return self._name

    @property
    def unit_of_measurement(self):
        """Return the unit of measurement."""
        return self._unit_of_measurement

    @property
    def precision(self):
        """Return the precision of the system."""
        return PRECISION_WHOLE

    @property
    def state(self):
        """Return the state of the sensor."""
        return self._state

    @property
    def device_state_attributes(self):
        """Return the state attributes."""
        return self._attributes    

    def update(self):
        """Fetch new state data for the sensor."""
        self._data.update()
        value = self._data.total
        if value is None:
            value = STATE_UNKNOWN
            self._attributes = {}
        else:
            self._state = value   
            self._attributes = self._data.attrs 

    

class UnifiSensorData(object):
    """
    Unifi API for the Unifi Controller.
    """
    _login_data = {}
    _current_status_code = None

    def __init__(self, hass, username, password, site, baseurl, verify_ssl):
        """
        Initiates tha api with default settings if none other are set.
        :param username: username for the controller user
        :param password: password for the controller user
        :param site: which site to connect to (Not the name you've given the site, but the url-defined name)
        :param baseurl: where the controller is located
        :param verify_ssl: Check if certificate is valid or not, throws warning if set to False
        """
        self._hass = hass
        self._login_data['username'] = username
        self._login_data['password'] = password
        self._site = site
        self._verify_ssl = verify_ssl
        self._baseurl = baseurl
        self._session = Session()
        self._ap_list = {}
        self.total = 0
        self.attrs = {}

    def __enter__(self):
        """
        Contextmanager entry handle
        :return: isntance object of class
        """
        self.login()
        return self

    def __exit__(self, *args):
        """
        Contextmanager exit handle
        :return: None
        """
        self.logout()

    def login(self):
        """
        Log the user in
        :return: None
        """
        self._current_status_code = self._session.post("{}/api/login".format(self._baseurl), data=json.dumps(self._login_data), verify=self._verify_ssl).status_code

        if self._current_status_code == 400:
            _LOGGER.error("Failed to log in to api with provided credentials")

    def logout(self):
        """
        Log the user out
        :return: None
        """
        self._session.get("{}/logout".format(self._baseurl))
        self._session.close()

    def list_clients(self) -> list:
        """
        List all available clients from the api
        :return: A list of clients on the format of a dict
        """

        r = self._session.get("{}/api/s/{}/stat/sta".format(self._baseurl, self._site, verify=self._verify_ssl), data="json={}")
        self._current_status_code = r.status_code
        
        if self._current_status_code == 401:
            _LOGGER.error("Unifi: Invalid login, or login has expired")
            return None

        data = r.json()['data']

        return data

    def list_devices(self, mac=None) -> list:
        """
        List all available devices from the api
        :param mac: if defined, return information for this device only
        :return: A list of devices on the format of a dict
        """

        r = self._session.get("{}/api/s/{}/stat/device/{}".format(self._baseurl, self._site, mac, selfverify=self._verify_ssl), data="json={}")
        self._current_status_code = r.status_code
        
        if self._current_status_code == 401:
            _LOGGER.error("Unifi: Invalid login, or login has expired")
            return None

        data = r.json()['data']

        return data

    def update_ap_list(self, newmac):

        device_info = (self.list_devices(mac=newmac))
        try:
            self._ap_list[newmac] = "AP_" + device_info[0]['name']
        except:
            self._ap_list[newmac] = newmac


    def update(self):
        self.login()
        self.total = 0
        self.attrs = {}
        devices_per_essid = {}
        devices_per_ap = {}
        devices_per_ap_name = {}
        devices_wired = 0

        device_list = (self.list_clients())
        for device in device_list:
          self.total += 1
          try:
            if device['is_wired']:
                devices_wired += 1
            else:    
                if device['essid'] in devices_per_essid.keys():
                  devices_per_essid[device['essid']] += 1   
                else:
                  devices_per_essid[device['essid']] = 1   
                if device['ap_mac'] in devices_per_ap.keys():
                  devices_per_ap[device['ap_mac']] += 1   
                else:
                  devices_per_ap[device['ap_mac']] = 1   
          except:
            _LOGGER.error("error processing device %s", device["mac"])

        for ap in devices_per_ap.keys():
            if ap in self._ap_list.keys():    
               devices_per_ap_name[self._ap_list[ap]] = devices_per_ap[ap]
            else:
               self.update_ap_list(ap)
               devices_per_ap_name[self._ap_list[ap]] = devices_per_ap[ap]   

        #update attrs
        for key in devices_per_essid.keys():
          self.attrs[key] = devices_per_essid[key]
        for key in devices_per_ap_name.keys():
          self.attrs[key] = devices_per_ap_name[key]
        if devices_wired > 0:
          self.attrs['wired'] =devices_wired 

self.logout() 

thanks

Hi,

hm… not sure what’s happening. But please try to remove the “region:” key from config.

This sensor is awesome! @clyra

  1. Does “region” = “site” in UniFi? If so, what’s the exact syntax that should be used? the name of the site or the code in the URL? e.g. I’m not using the default site for my set up.
    UPDATE - I’ve confirmed this requires the SITE ID when not using the default. This would be a good thing to note for those not using the default site.

  2. URL & SSL - my setup does not have an SSL cert configured, but I can access it externally through SSL via reverse proxy. Should I be using the external URL or the local one with port number?
    UPDATE - once I changed the “region” to the SITE ID, I confirmed this works for either URL.

Would it be possible to pull out a single AP from this sensor so you can quickly see if its status?

1 Like

Hi,

yes, region = site. I just re-used a know configuration key from HA. Nice you get it working. There are a lot of things I just didn’t thought people would do/want :-). By seeing the status of a single ap, what you mean?

I have an AP that is meshed to the rest of my network. It’d be great if I could build automation around its status. If offline, do x.

Excited to see this working!

hm… ok. I will look into it, meanwhile you have two other options: 1) use the current sensor and see if there’s one or more clients connected to it (look into the attributes), or )2) use the ping sensor and see if it’s online.

1 Like

Thanks for this. Going to try it out tonight. Cheers

Initially had trouble, but got it going by removing ssl.

@clyra thank you for sharing this component.
would you please consider adding the json tracker to use with the below component which provides immediate updates on any changes you may effect.

Sure! I guess that next week I will have time to look how to do it.

Hi,

Thanks for sharing this component! I was trying to set this up to simply track when guests are present or not but am finding that only networks with devices connected show up in the attributes of the sensor, but if no-one is connected to the guest network the attribute for that network disappears.

This has meant that my binary template sensor runs into errors whenever no-one is connected to the guest network because the attribute no longer exists

Could this behaviour change in the sensor so that all networks still exist as attributes but show as ‘0’ until a device connects?

Hope this makes sense!
Cheers,
James