From 4f765b610929e0d1893e6028ccaacafff8aa1101 Mon Sep 17 00:00:00 2001 From: Devmel Apps Date: Tue, 8 Aug 2023 17:24:59 +0200 Subject: [PATCH] RF listening and states managements on callback --- CHANGELOG.md | 13 ++ addons/airsend/callback.php | 81 +++----- addons/airsend/config.yaml | 8 +- addons/airsend/hassapi.class.php | 330 +++++++++++++++++++++++-------- addons/airsend/run.sh | 1 + 5 files changed, 300 insertions(+), 133 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bd102d7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +## 1.2 + +- RF listening and states managements on callback + +## 1.1 + +- Repository Installation + +## 1.0 + +- Initial release diff --git a/addons/airsend/callback.php b/addons/airsend/callback.php index 2bd6891..a7a488a 100644 --- a/addons/airsend/callback.php +++ b/addons/airsend/callback.php @@ -21,58 +21,41 @@ $data = json_decode($raw, true, 512, JSON_BIGINT_AS_STRING); if (is_array($data) && isset($data['events'])) { foreach ($data['events'] as $i => $val) { if(isset($val['channel']) && isset($val['type']) && isset($val['thingnotes'])){ - $entity_id = $api->searchEntityId($val['thingnotes']['uid']); - if(isset($entity_id)){ - //Transfer event - if($val['type'] == 3 || $val['type'] == 2 || $val['type'] == 1){ - $notes = $val['thingnotes']['notes']; - if(is_array($notes) && count($notes) > 0){ - foreach ($notes as $i => $note) { - $ovalue = $note['value']; - if($note['type'] == 0){ //STATE - if(is_numeric($ovalue)){ - $ovalue = intval($ovalue); - $state = null; - $position = 0; - switch($ovalue){ - case 18: //TOGGLE - $state = 'pressed'; - break; - case 19: //OFF - $position = 0; - break; - case 20: //ON - $position = 100; - break; - case 17: //STOP - $state = 'stop'; - break; - case 33: //MIDDLE - case 38: //USERPOS - $state = 'user'; - break; - case 34: //DOWN - $position = 0; - break; - case 35: //UP - $position = 100; - break; - } - if($state){ - $api->setState($entity_id, $state); - }else{ - $api->setPosition($entity_id, $position); - } - } - }else if($note['type'] == 1){ //DATA - $api->setState($entity_id, 'pressed'); - }else if($note['type'] == 9){ //LEVEL - $api->setPosition($entity_id, intval($ovalue)); + //Transfer event + if(isset($val['thingnotes']['uid'])){ + $entity_id = $api->searchEntityId($val['thingnotes']['uid']); + if(isset($entity_id)){ + if($val['type'] == 3 || $val['type'] == 2 || $val['type'] == 1){ + $states = $api->convertNotesToStates($val['thingnotes']['notes']); + foreach ($states as $j => $state) { + $api->setState($entity_id, $state[0], $state[1], $val['timestamp']); + } + }else{ + $api->setState($entity_id, 'error', 'error_'.$val['type'], $val['timestamp']); + } + } + //Interrupt event + }else{ + if($val['type'] == 3){ //Event type GOT (sensor) + $isreliable = false; + if($val['reliability'] > 0x6 && $val['reliability'] < 0x47){ + $isreliable = true; + } + if($isreliable == true){ + $states = $api->convertNotesToStates($val['thingnotes']['notes']); + foreach ($states as $j => $state) { + //Search entity and update + $entities = $api->searchEntitiesFromChannelAndType($val['channel'], $state[0]); + foreach ($entities as $k => $entity_id) { + $api->setState($entity_id, $state[0], $state[1], $val['timestamp']); + } + //Creates if not exists + if(count($entities) == 0){ + trigger_error("new channel found : ".json_encode($val['channel'])." ".$state[0]." ".$state[1], E_USER_NOTICE); + $api->setState(null, $state[0], $state[1], $val['timestamp'], $val['channel']); } } } - }else{ - $api->setState($entity_id, 'error_'.$val['type']); } } } diff --git a/addons/airsend/config.yaml b/addons/airsend/config.yaml index 620c160..4586fc0 100644 --- a/addons/airsend/config.yaml +++ b/addons/airsend/config.yaml @@ -1,6 +1,6 @@ name: "AirSend" -description: "AirSend Web Service" -version: "1.1" +description: "AirSend Web Service (local connection, no internet, no cloud)" +version: "1.2" slug: "airsend" url: https://github.com/devmel/hass_airsend-addon codenotary: notary@home-assistant.io @@ -17,3 +17,7 @@ ports: 33863/tcp: null ports_description: 33863/tcp: AirSendWebService communication +options: + auto_include: true +schema: + auto_include: bool diff --git a/addons/airsend/hassapi.class.php b/addons/airsend/hassapi.class.php index 81d31aa..08f4d38 100644 --- a/addons/airsend/hassapi.class.php +++ b/addons/airsend/hassapi.class.php @@ -6,8 +6,11 @@ class HassAPI{ function __construct($BASE_HASS_API, $HASS_API_TOKEN) { $this->BASE_HASS_API = $BASE_HASS_API; $this->HASS_API_TOKEN = $HASS_API_TOKEN; + $this->auto_include = false; if($this->isAuthorized()){ - $this->loadStates(); + $fbvalue = @file_get_contents('auto_include.cfg'); + $this->auto_include = filter_var($fbvalue, FILTER_VALIDATE_BOOLEAN); + $this->loadStates(); } } @@ -17,8 +20,251 @@ class HassAPI{ } return false; } + public function isAutoInclude() { + return $this->auto_include; + } + function searchEntityId($uid) { + if($this->states){ + foreach ($this->states as $value) { + if($value['entity_uid_sha256'] == $uid){ + return $value['entity_id']; + } + } + } + return null; + } - function loadStates() { + function searchEntitiesFromChannelAndType($channel, $type) { + $ret = array(); + if(isset($channel) && $this->states){ + foreach ($this->states as $value) { + if(array_key_exists('attributes', $value) && array_key_exists('channel', $value['attributes'])){ + if(self::isChannelCompatible($channel, $value['attributes']['channel'])){ + $foundid = false; + $entity_id = $value['entity_id']; + //Sensors + $is_sensor = (stripos($entity_id, "sensor.") !== false) ? true : false; + if($is_sensor && array_key_exists('device_class', $value['attributes'])){ + $dclass = $value['attributes']['device_class']; + $dclass = str_replace('humidity', 'r_humidity', $dclass); + if($dclass == $type){ + $foundid = true; + } + } + //Others + if($type == 'level' || $type == 'state' || $type == 'toggle' || $type == 'data'){ + $foundid = true; + } + if($foundid){ + $ret[] = $entity_id; + } + } + } + } + } + return $ret; + } + + function setState($entity_id, $type, $state, $timestamp_ms, $channel = null) { + $attributes = null; + if($entity_id == null && isset($channel)){ + //New entity + $entity_id = "sensor.".self::toUniqueChannelName($channel); + $attributes = array('channel' => $channel); + if($type === 'temperature'){ + $entity_id .= '_temp'; + $attributes['device_class'] = 'temperature'; + $attributes['unit_of_measurement'] = '°C'; + }else if($type === 'illuminance'){ + $entity_id .= '_ill'; + $attributes['device_class'] = 'illuminance'; + $attributes['unit_of_measurement'] = 'lx'; + }else if($type === 'r_humidity'){ + $entity_id .= '_rh'; + $attributes['device_class'] = 'humidity'; + $attributes['unit_of_measurement'] = '%'; + }else if($type === 'toggle' || $type === 'data'){ + //auto_include disabled for toggle, it's too messy on dashboard + return ; + } + } + $content = $this->getState($entity_id); + if($content == null && $attributes != null && $this->auto_include){ + $content = array('attributes' => $attributes); + } + if($content != null){ + if($attributes != null){ + $content['attributes'] = array_merge($content['attributes'], $attributes); + } + $current_position = null; + if($type === 'data'){ + if ($state && strlen($state) > 20){ + $state = substr($state, 0, 17) . '...'; + } + if(array_key_exists('state', $content) && $content['state'] == $state){ + $state .= '+'; + } + } + if($type === 'toggle'){ + if(array_key_exists('state', $content) && $content['state'] == 'pressed'){ + $state .= '+'; + } + } + if($type === 'level'){ + $is_cover = (stripos($entity_id, "cover.") !== false) ? true : false; + $current_position = intval($state); + $state = $current_position.'%'; + if($current_position >= 100){ + $state = $is_cover ? 'open': 'on'; + }else if($current_position <= 0){ + $state = $is_cover ? 'closed': 'off'; + } + } + $content['state'] = $state; + if($current_position !== null){ + if(array_key_exists('attributes', $content) && array_key_exists('supported_features', $content['attributes']) && ($content['attributes']['supported_features']&0x4) == 0x4){ + $content['attributes']['current_position'] = $current_position; + } + } + unset($content['entity_uid_sha256']); + unset($content['last_changed']); + unset($content['last_updated']); + //$dti = DateTimeImmutable::createFromFormat('U.u', ($timestamp_ms/1000)); + //$content['last_updated'] = $dti->format('Y-m-d').'T'.$dti->format('H:i:s').'+00:00'; + $this->postState($entity_id, $content); + return true; + } + return false; + } + + function getState($entity_id){ + if($this->states && isset($entity_id)){ + foreach ($this->states as $value) { + if($value['entity_id'] == $entity_id){ + return $value; + } + } + } + return null; + } + + public static function convertNotesToStates($notes){ + $ret = array(); + if(is_array($notes) && count($notes) > 0){ + foreach ($notes as $i => $note) { + $ovalue = $note['value']; + $stype = null; + $svalue = null; + if($note['type'] == 0){ //STATE + if(!is_numeric($ovalue)){ + $statemap = array("","PING","PROG","UNPROG","RESET", + "","","","","","","","","","","","","STOP","TOGGLE","OFF","ON","CLOSE","OPEN", + "","","","","","","","","","","MIDDLE","DOWN","UP","LEFT","RIGHT","USERPOSITION"); + $ovalue = array_search($ovalue, $statemap); + } + if(is_numeric($ovalue)){ + $stype = 'state'; + switch(intval($ovalue)){ + case 18: //TOGGLE + $stype = 'toggle'; + $svalue = 'pressed'; + break; + case 19: //OFF + $stype = 'level'; + $svalue = 0; + break; + case 20: //ON + $stype = 'level'; + $svalue = 100; + break; + case 17: //STOP + $svalue = 'stop'; + break; + case 33: //MIDDLE + case 38: //USERPOS + $svalue = 'user'; + break; + case 34: //DOWN + $stype = 'level'; + $svalue = 0; + break; + case 35: //UP + $stype = 'level'; + $svalue = 100; + break; + } + } + }else if($note['type'] == 1){ //DATA + $stype = 'data'; + $svalue = $ovalue; + }else if($note['type'] == 2){ //TEMPERATURE + $stype = 'temperature'; + $svalue = (floor((floatval($ovalue) - 273.15) * 10.0 + 0.5) / 10.0); //Kelvins to celcius + }else if($note['type'] == 3){ //ILLUMINANCE + $stype = 'illuminance'; + $svalue = floatval($ovalue); + }else if($note['type'] == 4){ //R_HUMIDITY + $stype = 'r_humidity'; + $svalue = intval($ovalue); + }else if($note['type'] == 9){ //LEVEL + $stype = 'level'; + $svalue = intval($ovalue); + } + if($stype != null){ + $ret[] = array($stype, $svalue); + } + } + } + return $ret; + } + + public static function isChannelCompatible($obj1, $obj2){ + $result = false; + if(isset($obj1) && isset($obj2)){ + $ch1 = self::toBasicChannel($obj1); + $ch2 = self::toBasicChannel($obj2); + $result = ($ch1 == $ch2); + } + return $result; + } + + public static function toUniqueChannelName($channel){ + $result = $channel['id']; + if($result){ + $uniquefield = array('source', 'mac', 'seed'); + foreach ($uniquefield as $field) { + if(array_key_exists($field, $channel)){ + $result .= "_"; + $result .= $channel[$field]; + } + } + } + return $result; + } + + public static function toBasicChannel($channel){ + $result = array(); + $uniquefield = array('id', 'source'); + foreach ($channel as $key => $value) { + if (in_array($key, $uniquefield)) { + $result[$key] = $value; + } + } + return $result; + } + + private function postState($entity_id, $content){ + $json = json_encode($content, JSON_FORCE_OBJECT); + if(strpos($this->BASE_HASS_API, "supervisor") !== false){ + //Bug in supervisor proxy => use bash + exec('./poststate.sh "'.$entity_id.'" "'.base64_encode($json).'"', $output, $retval); + }else{ + $res = HassAPI::request($this->BASE_HASS_API."/states/".$entity_id, $json, 'POST', $this->HASS_API_TOKEN); + } + trigger_error("postState ".$entity_id." ".$content['state'], E_USER_NOTICE); + } + + private function loadStates() { $res = HassAPI::request($this->BASE_HASS_API."/states", null, 'GET', $this->HASS_API_TOKEN); if($res !== false && isset($res) && is_array($res) && isset($res['data'])){ $this->states = json_decode($res['data'], true); @@ -32,86 +278,6 @@ class HassAPI{ } } - function getState($entity_id){ - if($this->states){ - foreach ($this->states as $value) { - if($value['entity_id'] == $entity_id){ - return $value; - } - } - } - return null; - } - - function searchEntityId($uid) { - if($this->states){ - foreach ($this->states as $value) { - if($value['entity_uid_sha256'] == $uid){ - return $value['entity_id']; - } - } - } - return null; - } - - function setState($entity_id, $state) { - $content = $this->getState($entity_id); - if($content != null){ - //Update state - $content['state'] = $state; - if(($content['attributes']['supported_features']&0x4) == 0x4){ - $position = 50; - if($state == 'open'){ - $position = 100; - }else if($state == 'closed'){ - $position = 0; - } - $content['attributes']['current_position'] = $position; - } - unset($content['entity_uid_sha256']); - unset($content['last_changed']); - unset($content['last_updated']); - $this->postState($entity_id, $content); - return true; - } - return false; - } - - function setPosition($entity_id, $position) { - $content = $this->getState($entity_id); - if($content != null){ - $is_cover = (stripos($entity_id, "cover.") !== false) ? true : false; - //Update position - $state = 'stop'; - if(($content['attributes']['supported_features']&0x4) == 0x4){ - $content['attributes']['current_position'] = $position; - $state = $position.'%'; - } - if($position == 100){ - $state = $is_cover ? 'open': 'on'; - }else if($position == 0){ - $state = $is_cover ? 'closed': 'off'; - } - $content['state'] = $state; - unset($content['entity_uid_sha256']); - unset($content['last_changed']); - unset($content['last_updated']); - $this->postState($entity_id, $content); - return true; - } - return false; - } - - private function postState($entity_id, $content){ - $json = json_encode($content, JSON_FORCE_OBJECT); - if(strpos($this->BASE_HASS_API, "supervisor") !== false){ - //Bug in supervisor proxy => use bash - exec('./poststate.sh "'.$entity_id.'" "'.base64_encode($json).'"', $output, $retval); - }else{ - $res = HassAPI::request($this->BASE_HASS_API."/states/".$entity_id, $json, 'POST', $this->HASS_API_TOKEN); - } - trigger_error("postState ".$entity_id." ".$content['state'], E_USER_NOTICE); - } private static function request($url, $data = null, $method = 'GET', $token = null) { $result = false; diff --git a/addons/airsend/run.sh b/addons/airsend/run.sh index 6748729..cbb542f 100644 --- a/addons/airsend/run.sh +++ b/addons/airsend/run.sh @@ -16,6 +16,7 @@ echo "internal_url: http://${hname}:33863/" if [ -n "${SUPERVISOR_TOKEN:-}" ] then echo ${SUPERVISOR_TOKEN} > hass_api.token + echo $(bashio::config 'auto_include') > auto_include.cfg else echo "Not running on Home Assistant machine..." fi