From 3c59791b2e74e214dfc85146471328c7f0b61742 Mon Sep 17 00:00:00 2001 From: MeIchthys <10717998+meichthys@users.noreply.github.com> Date: Tue, 24 Mar 2020 06:11:35 -0400 Subject: [PATCH] Add Nextcloud Integration (#30871) * some sensors working in homeassistant * bring up to date * add codeowner * update requirements * overhaul data imports from api & sensor discovery * remove print statement * delete requirements_test_all * add requrements_test_all.txt * Update homeassistant/components/nextcloud/sensor.py Co-Authored-By: springstan <46536646+springstan@users.noreply.github.com> * Update homeassistant/components/nextcloud/sensor.py Co-Authored-By: springstan <46536646+springstan@users.noreply.github.com> * describe recursive function * clarify that dict is returned * remove requirements from requirements_test_all * improve and simplify sensor naming * add basic tests * restore pre-commit config * update requirements_test_all * remove codespell requirement * update pre-commit-config * add-back codespell * rename class variables as suggested by @springstan * add dev branch to no-commit-to-branch git hook Because my fork had the same 'dev' branch i wasn't able to push. Going forward I should probably name my branches differently. * move config logic to __init__.py * restore .pre-commit-config.yaml * remove tests * remove nextcloud test requirement * remove debugging code * implement binary sensors * restore .pre-commit-config.yaml * bump dependency version * bump requirements files * bump nextcloud reqirement to latest * update possible exceptions, use fstrings * add list of sensors & fix inconsistency in get_data_points * use domain for config * fix guard clause * repair pre-commit-config * Remove period from logging * include url in unique_id * update requirements_all.txt Co-authored-by: springstan <46536646+springstan@users.noreply.github.com> --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/nextcloud/__init__.py | 147 ++++++++++++++++++ .../components/nextcloud/binary_sensor.py | 52 +++++++ .../components/nextcloud/manifest.json | 12 ++ homeassistant/components/nextcloud/sensor.py | 52 +++++++ requirements_all.txt | 3 + 7 files changed, 268 insertions(+) create mode 100644 homeassistant/components/nextcloud/__init__.py create mode 100644 homeassistant/components/nextcloud/binary_sensor.py create mode 100644 homeassistant/components/nextcloud/manifest.json create mode 100644 homeassistant/components/nextcloud/sensor.py diff --git a/.coveragerc b/.coveragerc index cc04fb03456..a218c812df4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -468,6 +468,7 @@ omit = homeassistant/components/netgear_lte/* homeassistant/components/netio/switch.py homeassistant/components/neurio_energy/sensor.py + homeassistant/components/nextcloud/* homeassistant/components/nfandroidtv/notify.py homeassistant/components/niko_home_control/light.py homeassistant/components/nilu/air_quality.py diff --git a/CODEOWNERS b/CODEOWNERS index 86a36551f57..e425a4d2d1c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -247,6 +247,7 @@ homeassistant/components/netatmo/* @cgtobi homeassistant/components/netdata/* @fabaff homeassistant/components/nexia/* @ryannazaretian @bdraco homeassistant/components/nextbus/* @vividboarder +homeassistant/components/nextcloud/* @meichthys homeassistant/components/nilu/* @hfurubotten homeassistant/components/nissan_leaf/* @filcole homeassistant/components/nmbs/* @thibmaek diff --git a/homeassistant/components/nextcloud/__init__.py b/homeassistant/components/nextcloud/__init__.py new file mode 100644 index 00000000000..39eb16ec265 --- /dev/null +++ b/homeassistant/components/nextcloud/__init__.py @@ -0,0 +1,147 @@ +"""The Nextcloud integration.""" +from datetime import timedelta +import logging + +from nextcloudmonitor import NextcloudMonitor, NextcloudMonitorError +import voluptuous as vol + +from homeassistant.const import ( + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + CONF_URL, + CONF_USERNAME, +) +from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers.event import track_time_interval + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "nextcloud" +NEXTCLOUD_COMPONENTS = ("sensor", "binary_sensor") +SCAN_INTERVAL = timedelta(seconds=60) + +# Validate user configuration +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_URL): cv.url, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) +BINARY_SENSORS = ( + "nextcloud_system_enable_avatars", + "nextcloud_system_enable_previews", + "nextcloud_system_filelocking.enabled", + "nextcloud_system_debug", +) + +SENSORS = ( + "nextcloud_system_version", + "nextcloud_system_theme", + "nextcloud_system_memcache.local", + "nextcloud_system_memcache.distributed", + "nextcloud_system_memcache.locking", + "nextcloud_system_freespace", + "nextcloud_system_cpuload", + "nextcloud_system_mem_total", + "nextcloud_system_mem_free", + "nextcloud_system_swap_total", + "nextcloud_system_swap_free", + "nextcloud_system_apps_num_installed", + "nextcloud_system_apps_num_updates_available", + "nextcloud_system_apps_app_updates_calendar", + "nextcloud_system_apps_app_updates_contacts", + "nextcloud_system_apps_app_updates_tasks", + "nextcloud_system_apps_app_updates_twofactor_totp", + "nextcloud_storage_num_users", + "nextcloud_storage_num_files", + "nextcloud_storage_num_storages", + "nextcloud_storage_num_storages_local", + "nextcloud_storage_num_storage_home", + "nextcloud_storage_num_storages_other", + "nextcloud_shares_num_shares", + "nextcloud_shares_num_shares_user", + "nextcloud_shares_num_shares_groups", + "nextcloud_shares_num_shares_link", + "nextcloud_shares_num_shares_mail", + "nextcloud_shares_num_shares_room", + "nextcloud_shares_num_shares_link_no_password", + "nextcloud_shares_num_fed_shares_sent", + "nextcloud_shares_num_fed_shares_received", + "nextcloud_shares_permissions_3_1", + "nextcloud_server_webserver", + "nextcloud_server_php_version", + "nextcloud_server_php_memory_limit", + "nextcloud_server_php_max_execution_time", + "nextcloud_server_php_upload_max_filesize", + "nextcloud_database_type", + "nextcloud_database_version", + "nextcloud_database_version", + "nextcloud_activeusers_last5minutes", + "nextcloud_activeusers_last1hour", + "nextcloud_activeusers_last24hours", +) + + +def setup(hass, config): + """Set up the Nextcloud integration.""" + # Fetch Nextcloud Monitor api data + conf = config[DOMAIN] + + try: + ncm = NextcloudMonitor(conf[CONF_URL], conf[CONF_USERNAME], conf[CONF_PASSWORD]) + except NextcloudMonitorError: + _LOGGER.error("Nextcloud setup failed - Check configuration") + + hass.data[DOMAIN] = get_data_points(ncm.data) + hass.data[DOMAIN]["instance"] = conf[CONF_URL] + + def nextcloud_update(event_time): + """Update data from nextcloud api.""" + try: + ncm.update() + except NextcloudMonitorError: + _LOGGER.error("Nextcloud update failed") + return False + + hass.data[DOMAIN] = get_data_points(ncm.data) + + # Update sensors on time interval + track_time_interval(hass, nextcloud_update, conf[CONF_SCAN_INTERVAL]) + + for component in NEXTCLOUD_COMPONENTS: + discovery.load_platform(hass, component, DOMAIN, {}, config) + + return True + + +# Use recursion to create list of sensors & values based on nextcloud api data +def get_data_points(api_data, key_path="", leaf=False): + """Use Recursion to discover data-points and values. + + Get dictionary of data-points by recursing through dict returned by api until + the dictionary value does not contain another dictionary and use the + resulting path of dictionary keys and resulting value as the name/value + for the data-point. + + returns: dictionary of data-point/values + """ + result = {} + for key, value in api_data.items(): + if isinstance(value, dict): + if leaf: + key_path = f"{key}_" + if not leaf: + key_path += f"{key}_" + leaf = True + result.update(get_data_points(value, key_path, leaf)) + else: + result[f"{DOMAIN}_{key_path}{key}"] = value + leaf = False + return result diff --git a/homeassistant/components/nextcloud/binary_sensor.py b/homeassistant/components/nextcloud/binary_sensor.py new file mode 100644 index 00000000000..9e4c6f5d969 --- /dev/null +++ b/homeassistant/components/nextcloud/binary_sensor.py @@ -0,0 +1,52 @@ +"""Summary binary data from Nextcoud.""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice + +from . import BINARY_SENSORS, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Nextcloud sensors.""" + if discovery_info is None: + return + binary_sensors = [] + for name in hass.data[DOMAIN]: + if name in BINARY_SENSORS: + binary_sensors.append(NextcloudBinarySensor(name)) + add_entities(binary_sensors, True) + + +class NextcloudBinarySensor(BinarySensorDevice): + """Represents a Nextcloud binary sensor.""" + + def __init__(self, item): + """Initialize the Nextcloud binary sensor.""" + self._name = item + self._is_on = None + + @property + def icon(self): + """Return the icon for this binary sensor.""" + return "mdi:cloud" + + @property + def name(self): + """Return the name for this binary sensor.""" + return self._name + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._is_on == "yes" + + @property + def unique_id(self): + """Return the unique ID for this binary sensor.""" + return f"{self.hass.data[DOMAIN]['instance']}#{self._name}" + + def update(self): + """Update the binary sensor.""" + self._is_on = self.hass.data[DOMAIN][self._name] diff --git a/homeassistant/components/nextcloud/manifest.json b/homeassistant/components/nextcloud/manifest.json new file mode 100644 index 00000000000..4db0019920d --- /dev/null +++ b/homeassistant/components/nextcloud/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "nextcloud", + "name": "Nextcloud", + "documentation": "https://www.home-assistant.io/integrations/nextcloud", + "requirements": [ + "nextcloudmonitor==1.1.0" + ], + "dependencies": [], + "codeowners": [ + "@meichthys" + ] +} \ No newline at end of file diff --git a/homeassistant/components/nextcloud/sensor.py b/homeassistant/components/nextcloud/sensor.py new file mode 100644 index 00000000000..aacd33ec3e8 --- /dev/null +++ b/homeassistant/components/nextcloud/sensor.py @@ -0,0 +1,52 @@ +"""Summary data from Nextcoud.""" +import logging + +from homeassistant.helpers.entity import Entity + +from . import DOMAIN, SENSORS + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Nextcloud sensors.""" + if discovery_info is None: + return + sensors = [] + for name in hass.data[DOMAIN]: + if name in SENSORS: + sensors.append(NextcloudSensor(name)) + add_entities(sensors, True) + + +class NextcloudSensor(Entity): + """Represents a Nextcloud sensor.""" + + def __init__(self, item): + """Initialize the Nextcloud sensor.""" + self._name = item + self._state = None + + @property + def icon(self): + """Return the icon for this sensor.""" + return "mdi:cloud" + + @property + def name(self): + """Return the name for this sensor.""" + return self._name + + @property + def state(self): + """Return the state for this sensor.""" + return self._state + + @property + def unique_id(self): + """Return the unique ID for this sensor.""" + return f"{self.hass.data[DOMAIN]['instance']}#{self._name}" + + def update(self): + """Update the sensor.""" + self._state = self.hass.data[DOMAIN][self._name] diff --git a/requirements_all.txt b/requirements_all.txt index 00a7a0a0073..141048c48b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -924,6 +924,9 @@ neurio==0.3.1 # homeassistant.components.nexia nexia==0.7.1 +# homeassistant.components.nextcloud +nextcloudmonitor==1.1.0 + # homeassistant.components.niko_home_control niko-home-control==0.2.1