-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
829 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
vendor |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,216 @@ | ||
<?php | ||
|
||
require __DIR__.'/vendor/autoload.php'; | ||
require_once __DIR__.'/vendor/netatmo/netatmo-api-php/src/Netatmo/autoload.php'; | ||
require_once __DIR__.'/Storage.php'; | ||
|
||
/** | ||
* Netatmo API wrapper | ||
*/ | ||
class Netatmo | ||
{ | ||
private $logger; | ||
private $client; | ||
private $isMocked; | ||
public $devices; | ||
private $todayTimestamp; | ||
private $storage; | ||
const MAX_REQUESTS_BY_MODULE = 50; | ||
const WAITING_TIME_BEFORE_NEXT_MODULE = 10; | ||
|
||
/** | ||
* Initialize a Netatmo wrapper | ||
* | ||
* @param array $config Associative array including application and account information | ||
*/ | ||
public function __construct($config) | ||
{ | ||
$this->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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
<?php | ||
require_once __DIR__.'/vendor/autoload.php'; | ||
|
||
/** | ||
* Storage (influxDB) wrapper | ||
*/ | ||
class Storage | ||
{ | ||
private $client = null; | ||
private $database = null; | ||
|
||
/** | ||
* Initialize a InfluxDB wrapper | ||
*/ | ||
public function __construct() | ||
{ | ||
$this->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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ." | ||
] | ||
} | ||
} |
Oops, something went wrong.