Ik ben bezig met een nieuwe server thuis (de oude zakte langzamerhand door z'n pootjes - daarover later meer). Bigger, better, faster natuurlijk, maar ook ... Linux (in plaats van Windows). Nou is dat al een uitdaging op zich (daarover later meer - alhoewel het installeren van de printer wel weer supereenvoudig sudo/copy/paste/klaar was), maar het meeste is nog wel redelijk 1:1 verkrijgbaar in beide smaken (Thunderbird, Firefox, LibreOffice, enz). Behalve ... de
Plugwise controller/server. Die bestaat alleen voor Windows. Daar moest dus wat anders voor komen. Nou had iemand daar al wel een
Python scriptje voor gemaakt, maar dat deed niet helemaal precies wat ik wilde, en uiteindelijk wil ik het toch direct via PHP aansturen/uitlezen, dus dan maar aan de knutsel en die hele Python bende porten naar een stukje PHP.
<?php
class Plugwise{
const PREFIX = "\x05\x05\x03\x03"; //start of message
const SUFFIX = "\r\n"; //end of message
const CMD_LENGTH = 4;
const COUNT_LENGTH = 4;
const MAC_LENGTH = 16;
const CRC_LENGTH = 4;
const PERIOD_1SEC = 1;
const PERIOD_8SEC = 8;
const PERIOD_1HOUR = 3600;
const PERIOD_DATA = [ //location of pulse count in data string
self::PERIOD_1SEC => [0,4],
self::PERIOD_8SEC => [4,4],
self::PERIOD_1HOUR => [8,8]
];
const PULSES_TO_WATT = 2.132475706; //magic number for converting pulses to Watts
public $debug = false;
public $deviceOptions =
'115200 min 0 -parenb -parodd -cmspar cs8 hupcl -cstopb cread clocal -crtscts -ignbrk -brkint ' .
'-ignpar -parmrk -inpck -istrip -inlcr -igncr -icrnl -ixon -ixoff -iuclc -ixany -imaxbel -iutf8 ' .
'-opost -olcuc -ocrnl -onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0 ' .
'-isig -icanon -iexten -echo -echoe -echok -echonl -noflsh -xcase -tostop ' .
'-echoprt -echoctl -echoke -flusho -extproc';
protected $_device = null;
protected $_timeout = null;
protected $_port = null;
/*
* Initialize object.
* @param string $device Device where the Stick is connected to.
* @param float $timeout Time to wait for the correct response (seconds).
*/
public function __construct($device = '/dev/ttyUSB0',$timeout = 5){
$this->_device = $device;
$this->_timeout = $timeout;
}
/*
* Print debug information if debug flag is set.
* @param string $str Debug information (line feed will be added).
*/
protected function debug($str){
if($this->debug){
for($i = 0; $i < 32; $i++) $str = str_replace(chr($i),"[$i]",$str);
for($i = 127; $i <= 255; $i++) $str = str_replace(chr($i),"[$i]",$str);
print($str . "\n");
}
}
/*
* Pad a string left-side with zeros.
* @param string $str
* @param int $length
* @return string
*/
protected function pad($str,$length){
return str_pad($str,$length,'0',STR_PAD_LEFT);
}
/*
* Calculate the checksum (CRC16) for a control string.
* @param string $str
* @return string 16-bit hex string.
*/
protected function checksum($str){
$crc = 0;
$length = strlen($str);
for($i = 0; $i < $length; $i++){
$crc = ($crc ^ ord($str[$i]) << 8) & 0xffff;
for ($j = 0; $j < 8; $j++) $crc = (($crc & 0x8000) ? ($crc << 1) ^ 0x1021 : $crc << 1) & 0xffff;
}
return $this->pad(strtoupper(dechex($crc)),self::CRC_LENGTH);
}
/*
* Write a command to the Stick.
* @param string $cmd Command (max 16-bit hex).
* @param string $mac MAC address of the Circle to write to.
* @param string $args Extra arguments for the command.
*/
protected function write($cmd,$mac = null,$args = null){
$this->debug("write(cmd: $cmd, mac: $mac, args: $args)");
if($mac && !preg_match('/^[\\dA-Z]{' . self::MAC_LENGTH . '}$/',$mac)) throw new \Exception("Invalid MAC address '$mac'");
$data = $this->pad($cmd,self::CMD_LENGTH) . $mac . $args;
fwrite($this->getPort(),$str = self::PREFIX . $data . $this->checksum($data) . self::SUFFIX);
$this->debug(" sent '$str'");
}
/*
* Read response from a cricle.
* @param string $cmd Expected response command (max 16-bit hex; leave empty for unknown).
* @param string $mac MAC address where the response is coming from (leave empty for unknown).
* @param int $length Length of the response data (excluding the prefix, command, counter, MAC address, checksum, and the
* suffix; leave empty for unknown).
* @return string Response data.
*/
protected function read($cmd = null,$mac = null,$length = null){
$this->debug("read(cmd: $cmd,mac: $mac, length: $length)");
$suffix = -strlen(self::SUFFIX);
$start = microtime(true);
do{
do{
do{
$str = null;
do{
if(($c = fgetc($this->getPort())) !== false) $str .= $c;
if((microtime(true) - $start) >= $this->_timeout)
throw new \Exception("Timeout while reading" . ($length ? " $length bytes" : '') . " from '{$this->_device}' (got '$str')");
}
while(substr($str,$suffix) != self::SUFFIX);
}
while(($i = strpos($str,self::PREFIX)) === false);
$data = substr($str,$i + strlen(self::PREFIX),$suffix - self::CRC_LENGTH);
$this->debug(" received '$data'");
if(($expected = $this->checksum($data)) != ($received = substr($str,$suffix - self::CRC_LENGTH,self::CRC_LENGTH)))
throw new \Exception("Invalid checksum for '$data' (expected '$expected', received '$received')");
}
while($cmd && (substr($data,0,self::CMD_LENGTH) != $this->pad($cmd,self::CMD_LENGTH)));
}
while($mac && (substr($data,self::CMD_LENGTH + self::COUNT_LENGTH,self::MAC_LENGTH) != $mac));
$data = substr($data,self::CMD_LENGTH + self::COUNT_LENGTH + self::MAC_LENGTH);
if($length && (strlen($data) != $length)) throw new \Exception("Invalid data length ('$data' != $length)");
return $data;
}
/*
* Initialize the Stick.
* @param bool $wait True to wait for the correct response.
*/
public function init($wait = true){
$this->write('A');
if($wait) $this->read('11',null,26);
}
/*
* Read calibration data for a Circle.
* @param string $mac MAC address of the Circle.
* @return array Calibration data (4 floats).
*/
public function calibrate($mac){
$this->write('26',$mac);
$data = $this->read('27',$mac,32);
$result = [];
foreach(str_split($data,8) as $hex) $result[] = unpack('G',hex2bin($hex))[1];
return $result;
}
/*
* Read the current power usage for a Circle.
* @param string $mac MAC address of the Circle.
* @param array $calibration Calibration data (4 floats; will be automaticly fetched when empty).
* @param int $period Period to use for the pulse reading (see PERIOD_* constants).
* @return float Power usage in Watts.
*/
public function power($mac,$calibration = null,$period = self::PERIOD_8SEC){
if(!array_key_exists($period,self::PERIOD_DATA)) throw new \Exception("Invalid period $period seconds");
$this->write('12',$mac);
$pulses = hexdec($data = substr($this->read('13',$mac,28),self::PERIOD_DATA[$period][0],self::PERIOD_DATA[$period][1]));
if($negative = preg_match('/^[89A-F]/',$data)) $pulses = (1 << (strlen($data) << 2)) - $pulses;
if(!$pulses) return 0.0;
if(!$calibration) $calibration = $this->calibrate($mac);
$power = ((((($pulses / $period + $calibration[3])^2) * $calibration[1]) + (($pulses / $period + $calibration[3]) * $calibration[0])) + $calibration[2]) * self::PULSES_TO_WATT;
return $negative ? -$power : $power;
}
/*
* Get current relay status of Circle.
* @param string $mac MAC address of the Circle.
* @return bool True = on, false = off.
*/
public function status($mac){
$this->write('23',$mac);
$data = $this->read('24',$mac,42);
return substr($data,16,2) == '01';
}
/*
* Switch the relay of a Circle.
* @param string $mac MAC address of the Circle.
* @param bool $status True = on, false = off, null = opposite of current status.
* @return bool New status (true = on, false = off).
*/
public function switch($mac,$status = null){
if($status === null) $status = !$this->status($mac);
$this->write('17',$mac,$status ? '01' : '00');
return $status;
}
protected function getPort(){
if($this->_port === null){
shell_exec('stty -F ' . $this->_device . ' ' . $this->deviceOptions);
if($this->_port = fopen($this->_device,'r+b')) stream_set_timeout($this->_port,ceil($this->_timeout));
}
return $this->_port;
}
}
Het duurt even voordat de (communicatie met de) Stick op gang is. Zelf heb ik dus een continu procesje draaien, die constant alle verbruiken ophaalt. Als er dan een vraag is (via een socket direct naar ditzelfde proces) kan het verbuik meteen uit het geheugen worden geserveerd (hooguit 10 seconden oud). Alleen het schakelen moet dan nog "direct" gebeuren, maar dat gaat vrij vlot (ook omdat de communicatie met de Stick steeds "warm" gehouden wordt).
Rob, zondag 15 maart 2020, 22:42