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