Skip to content

Commit

Permalink
Version 0.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
nioc committed Jun 1, 2019
1 parent 445b8f1 commit c6be4a6
Show file tree
Hide file tree
Showing 8 changed files with 829 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
vendor
216 changes: 216 additions & 0 deletions Netatmo.php
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);
}
}
122 changes: 122 additions & 0 deletions Storage.php
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);
}
}
}
16 changes: 16 additions & 0 deletions composer.json
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 ."
]
}
}
Loading

0 comments on commit c6be4a6

Please sign in to comment.