From c6be4a61a70d14a3404ac9e751522108b2b5f62b Mon Sep 17 00:00:00 2001 From: nioc Date: Sun, 2 Jun 2019 01:32:14 +0200 Subject: [PATCH] Version 0.1.0 --- .gitignore | 1 + Netatmo.php | 216 ++++++++++++++++++++++++++++ Storage.php | 122 ++++++++++++++++ composer.json | 16 +++ composer.lock | 388 ++++++++++++++++++++++++++++++++++++++++++++++++++ config.php | 16 +++ config.xml | 17 +++ index.php | 53 +++++++ 8 files changed, 829 insertions(+) create mode 100644 .gitignore create mode 100644 Netatmo.php create mode 100644 Storage.php create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 config.php create mode 100644 config.xml create mode 100644 index.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..22d0d82 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +vendor diff --git a/Netatmo.php b/Netatmo.php new file mode 100644 index 0000000..f952e06 --- /dev/null +++ b/Netatmo.php @@ -0,0 +1,216 @@ +logger = Logger::getLogger('Netatmo'); + $this->todayTimestamp = time(); + + // Initialize Netatmo client + $configNetatmo = []; + $configNetatmo['client_id'] = $config['client_id']; + $configNetatmo['client_secret'] = $config['client_secret']; + $configNetatmo['username'] = $config['username']; + $configNetatmo['password'] = $config['password']; + $configNetatmo['scope'] = Netatmo\Common\NAScopes::SCOPE_READ_STATION; + $this->isMocked = $config['mock']; + if ($this->isMocked) { + $this->logger->warn('API is mocked'); + } else { + $this->client = new Netatmo\Clients\NAWSApiClient($configNetatmo); + } + + // Initialize database access + $this->storage = new Storage(); + $this->storage->connect($config['host'], $config['port'], $config['database']); + } + + /** + * Authentication with Netatmo server (OAuth2) + * + * @return Authentication result + */ + public function getToken() + { + if (!$this->isMocked) { + try { + $this->logger->debug('Request token'); + $tokens = $this->client->getAccessToken(); + $this->logger->debug('Token reived'); + $this->logger->trace('Token: '.json_encode($tokens)); + } catch (Netatmo\Exceptions\NAClientException $e) { + $this->logger->error('An error occured while trying to retrieve your tokens'); + $this->logger->debug('Reason: '.$e->getMessage()); + return false; + } + } + return true; + } + + /** + * Retrieve user's Weather Stations Information + * + * @return Query result + */ + public function getStations() + { + try { + $this->logger->debug('Request stations'); + if (!$this->isMocked) { + $data = $this->client->getData(null, true); + } else { + $this->logger->debug('Mocked: mock/stations.json'); + $data = json_decode(file_get_contents('mock/stations.json'), true); + } + $this->logger->debug('Stations received'); + // $this->logger->trace('Stations: '.json_encode($data)); + } catch (Netatmo\Exceptions\NAClientException $ex) { + $this->logger->error('An error occured while retrieving data'); + $this->logger->debug('Reason: '.$e->getMessage()); + return false; + } + if (empty($data['devices'])) { + $this->logger->error('No devices affiliated to user'); + return false; + } + $this->devices = $data['devices']; + $this->logger->info('Found ' . count($this->devices) . ' devices'); + return true; + } + + /** + * Request measures for a specific device/module from provided timestamp + * + * @param int $startTimestamp (optional) starting timestamp of requested measurements + * @param array $device associative array including information about device to get measures + * @param array $module (optional) associative array including information about module to get measures + * @return void + */ + public function getMeasures($startTimestamp, $device, $module) + { + $deviceId = $device['_id']; + $deviceName = $device['station_name']; + // default values for module + $moduleId = null; + $moduleName = $device['module_name']; + $moduleType = $device['type']; + if ($module) { + // if module provided, override default values + $moduleId = $module['_id']; + $moduleName = $module['module_name']; + $moduleType = $module['type']; + } + $this->logger->info("Request measures for device: $deviceName, module: $moduleName ($moduleType)"); + // Requested data type depends on the module's type + switch ($moduleType) { + case 'NAMain': + //main indoor module + $type = 'temperature,Co2,humidity,noise,pressure'; + break; + case 'NAModule1': + // outdoor module + $type = 'temperature,humidity'; + break; + case 'NAModule2': + // wind gauge module + $type = 'WindStrength,WindAngle,GustStrength,GustAngle'; + break; + case 'NAModule3': + // rain gauge module + $type = 'rain'; + break; + default: + // other (including additional indoor module) + $type = 'temperature,Co2,humidity'; + break; + } + $fieldKeys = explode(',', $type); + + if ($startTimestamp) { + $lastTimestamp = $startTimestamp; + } else { + $lastTimestamp = time() - 24*3600*30; + // Get last fetched timestamp + try { + $last = $this->storage->get_last_fetch_date($deviceName . '-' . $moduleName, $fieldKeys[0]); + if ($last !== null) { + $lastTimestamp = $last; + } + } catch (Exception $e) { + $this->logger->error('Can not get last fetch timestamp'); + // Can not access database, exit script + return; + } + } + $hasError = false; + $requestCount = 0; + + // Get measures by requesting API until max requests is reached, all data are reiceved or an error occurs + do { + $requestCount++; + try { + $this->logger->debug('Starting timestamp: ' . $lastTimestamp . ' (' . date('Y-m-d H:i:sP', $lastTimestamp) . ')'); + if (!$this->isMocked) { + $measure = $this->client->getMeasure($deviceId, $moduleId, 'max', $type, $lastTimestamp, $this->todayTimestamp, 1024, false, true); + // file_put_contents("mock2/$moduleType.json", json_encode($measure)); + } else { + $this->logger->debug("Mocked: mock/$moduleType.json"); + $measure = json_decode(file_get_contents("mock/$moduleType.json"), true); + } + // $this->logger->trace('Measure: '. json_encode($measure)); + } catch (Netatmo\Exceptions\NAClientException $e) { + $hasError = true; + $this->logger->error("An error occured while retrieving device $deviceName / module: $moduleName ($moduleType) measurements"); + $this->logger->debug('Reason: '.$e->getMessage()); + } + + // Store module measures in database + $points = []; + foreach ($measure as $timestamp => $values) { + $dt = new DateTime(); + $dt->setTimestamp($timestamp); + // $this->logger->trace('Handling values for ' . $dt->format('Y-m-d H:i:sP')); + $fields = []; + foreach ($values as $key => $val) { + if (array_key_exists($key, $fieldKeys)) { + // $this->logger->trace('. ' . $fieldKeys[$key] . ': ' . $val); + $fields[$fieldKeys[$key]] = (float) $val; + } + } + array_push($points, $this->storage->createPoint($deviceName . '-' . $moduleName, $timestamp, null, [], $fields)); + $lastTimestamp = max($lastTimestamp, $timestamp); + // $this->logger->trace('Max timestamp is now : ' . $lastTimestamp . ' (' . date('Y-m-d H:i:sP', $lastTimestamp) . ' )'); + } + try { + $this->storage->writePoints($points); + } catch (Exception $e) { + $hasError = true; + $this->logger->error("Can not write device $deviceName / module: $moduleName ($moduleType) measurements"); + } + } while ($lastTimestamp <= $this->todayTimestamp && !$hasError && $requestCount < $this::MAX_REQUESTS_BY_MODULE); + // Wait some seconds before continue to avoid reaching user limit API + sleep($this::WAITING_TIME_BEFORE_NEXT_MODULE); + } +} diff --git a/Storage.php b/Storage.php new file mode 100644 index 0000000..75caca6 --- /dev/null +++ b/Storage.php @@ -0,0 +1,122 @@ +logger = Logger::getLogger('Storage'); + } + + /** + * Connect to InfluxDB database, create it if not existing + * + * @param string $host InfluxDB server hostname (exemple: 'localhost') + * @param string $port InfluxDB server listening port (exemple: '8086') + * @param string $database InfluxDB database used (exemple: 'netatmo') + * @return void + */ + public function connect($host, $port, $database) + { + $this->logger->debug("Connecting to database $database (http://$host:$port)"); + try { + $this->client = new InfluxDB\Client($host, $port); + $this->logger->trace('InfluxDB client created'); + $this->database = $this->client->selectDB($database); + $this->logger->trace('Database selected'); + } catch (Exception $e) { + $this->logger->error('Can not access database'); + $this->logger->debug($e->getMessage()); + throw new Exception($e, 1); + } + try { + $this->logger->trace('Check database exists'); + if (!$this->database->exists()) { + $this->logger->trace('Database does not exist'); + $this->database->create(); + $this->database->alterRetentionPolicy(new InfluxDB\Database\RetentionPolicy('autogen', '1825d', 1, true)); + $this->logger->info('Database created successfully'); + } + } catch (Exception $e) { + $this->logger->error('Can not create database'); + $this->logger->debug($e->getMessage()); + } + return; + } + + /** + * Get timestamp of last fetched value for specific measurement and field + * + * @param string $measurement Measurement to be retrieved + * @param string $field Fieldname to use + * @return int timestamp + */ + public function get_last_fetch_date($measurement, $field) + { + $this->logger->debug("Get last fetch date for $measurement (on field $field)"); + $last = null; + try { + // Request last data + $result = $this->database->query("SELECT last($field) FROM \"$measurement\""); + $points = $result->getPoints(); + if (count($points)) { + $last = strtotime($points[0]['time']); + } + } catch (Exception $e) { + $this->logger->error('Can not access database'); + $this->logger->debug($e->getMessage()); + throw new Exception($e, 1); + } + $this->logger->debug("Last data was fetched $last"); + return $last; + } + + /** + * Prepare InfluxDB point before insertion + * + * @param string $measurement + * @param int $timestamp + * @param float $value Main value + * @param array $tags Optionnal tags + * @param array $values Optionnal keys/values + * @return InfluxDB\Point + */ + public function createPoint($measurement, $timestamp, $value, $tags, $values) + { + // $this->logger->trace("Create point $timestamp ($measurement)"); + return new InfluxDB\Point( + $measurement, + $value, + $tags, + $values, + $timestamp + ); + } + + /** + * Write points to database + * + * @param InfluxDB\Point[] $points Array of points to write + * @return void + */ + public function writePoints($points) + { + $this->logger->debug('Writing '.count($points).' points'); + try { + return $this->database->writePoints($points, InfluxDB\Database::PRECISION_SECONDS); + } catch (Exception $e) { + $this->logger->error('Can not write data'); + $this->logger->debug($e->getMessage()); + throw new Exception($e, 1); + } + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..a575029 --- /dev/null +++ b/composer.json @@ -0,0 +1,16 @@ +{ + "name": "nioc/netatmo-collector", + "description": "Netatmo collector is a script for requesting measures from Netatmo devices", + "homepage": "https://github.com/nioc/netatmo-collector", + "version": "0.1.0", + "license": "AGPL-3.0-only", + "require": { + "influxdb/influxdb-php": "^1.14", + "apache/log4php": "^2.3" + }, + "scripts": { + "post-install-cmd": [ + "mkdir -p vendor/netatmo/netatmo-api-php && cd vendor/netatmo/netatmo-api-php && git clone https://github.com/Netatmo/Netatmo-API-PHP.git ." + ] + } +} \ No newline at end of file diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..575710f --- /dev/null +++ b/composer.lock @@ -0,0 +1,388 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "a3e835378601bbcec2b841d97c2c75a3", + "packages": [ + { + "name": "apache/log4php", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/apache/logging-log4php.git", + "reference": "cac428b6f67d2035af39784da1d1a299ef42fcf2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/apache/logging-log4php/zipball/cac428b6f67d2035af39784da1d1a299ef42fcf2", + "reference": "cac428b6f67d2035af39784da1d1a299ef42fcf2", + "shasum": "" + }, + "require": { + "php": ">=5.2.7" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/main/php/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "A versatile logging framework for PHP", + "homepage": "http://logging.apache.org/log4php/", + "keywords": [ + "log", + "logging", + "php" + ], + "time": "2012-10-26T09:13:25+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "6.3.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/407b0cb880ace85c9b63c5f9551db498cb2d50ba", + "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba", + "shasum": "" + }, + "require": { + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.4", + "php": ">=5.5" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", + "psr/log": "^1.0" + }, + "suggest": { + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.3-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2018-04-22T15:46:56+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "v1.3.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "time": "2016-12-20T10:07:11+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.5.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "9f83dded91781a01c63574e387eaa769be769115" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/9f83dded91781a01c63574e387eaa769be769115", + "reference": "9f83dded91781a01c63574e387eaa769be769115", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "time": "2018-12-04T20:46:45+00:00" + }, + { + "name": "influxdb/influxdb-php", + "version": "1.15.0", + "source": { + "type": "git", + "url": "https://github.com/influxdata/influxdb-php.git", + "reference": "bf3415f81962e1ab8c939bc1a08a85f500bead35" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/influxdata/influxdb-php/zipball/bf3415f81962e1ab8c939bc1a08a85f500bead35", + "reference": "bf3415f81962e1ab8c939bc1a08a85f500bead35", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.0", + "php": "^5.5 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7" + }, + "suggest": { + "ext-curl": "Curl extension, needed for Curl driver", + "stefanotorresi/influxdb-php-async": "An asyncronous client for InfluxDB, implemented via ReactPHP." + }, + "type": "library", + "autoload": { + "psr-4": { + "InfluxDB\\": "src/InfluxDB" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gianluca Arbezzano", + "email": "gianarb92@gmail.com" + }, + { + "name": "Daniel Martinez", + "email": "danimartcas@hotmail.com" + }, + { + "name": "Stephen Hoogendijk", + "email": "stephen@tca0.nl" + } + ], + "description": "InfluxDB client library for PHP", + "keywords": [ + "client", + "influxdata", + "influxdb", + "influxdb class", + "influxdb client", + "influxdb library", + "time series" + ], + "time": "2019-05-30T00:15:14+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "2.0.5", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/5601c8a83fbba7ef674a7369456d12f1e0d0eafa", + "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "~3.7.0", + "satooshi/php-coveralls": ">=1.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "time": "2016-02-11T07:05:27+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} diff --git a/config.php b/config.php new file mode 100644 index 0000000..af4768f --- /dev/null +++ b/config.php @@ -0,0 +1,16 @@ + false, + // influxDB database configuration + 'host' => 'localhost', + 'port' => '8086', + 'database' => 'netatmo', + // Netatmo application + 'client_id' => '', + 'client_secret' => '', + // Netatmo user account + 'username' => '', + 'password' => '' + ]; diff --git a/config.xml b/config.xml new file mode 100644 index 0000000..ecaccb7 --- /dev/null +++ b/config.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/index.php b/index.php new file mode 100644 index 0000000..7e39f7d --- /dev/null +++ b/index.php @@ -0,0 +1,53 @@ +#!/usr/bin/php +info('Start Netatmo collect'); +$logger->debug('Log level: '.$logger->getEffectiveLevel()->toString()); + +// Get optionnal start date from script argument +$startTimestamp = null; +if ($argc > 1) { + $startDate = date_create_from_format('Y-m-d', $argv[1]); + if ($startDate === false) { + $logger->error('Argument must be a valid start date as Y-m-d, provided: ' . $argv[1]); + exit(1); + } + $startDate->setTime(0, 0); + $startTimestamp = $startDate->getTimestamp(); + $logger->info('Provided start date: ' . $startDate->format('Y-m-d H:i:sP')); +} + +// Initialize wrapper +$netatmo = new Netatmo($config); + +// Authentication with Netatmo server (OAuth2) +if (!$netatmo->getToken()) { + exit(1); +} + +// Retrieve user's Weather Stations Information +if (!$netatmo->getStations()) { + exit(1); +} + +// For each stations request measures for every modules +foreach ($netatmo->devices as $device) { + $logger->debug('Handling device: ' . $device['station_name']); + + // First, get main indoor module + $netatmo->getMeasures($startTimestamp, $device, null); + + // Then for its modules + foreach ($device['modules'] as $module) { + $netatmo->getMeasures($startTimestamp, $device, $module); + } +} + +$logger->info('End Netatmo collect');