7 Commits
Author SHA1 Message Date
Devmel Apps 4f765b6109 RF listening and states managements on callback 2023-08-08 17:24:59 +02:00
Devmel Apps df1e19ec49 external machine 2023-08-04 15:01:45 +02:00
Devmel Apps 1cc2efecc8 internal_url 2023-07-28 13:47:59 +02:00
Devmel Apps 96191aa99a Repository Installation 2022-11-22 18:47:32 +01:00
Devmel Apps 84c3fd7b97 latest install 2022-11-22 15:05:50 +01:00
Devmel Apps f504335c5b asynchronous states 2022-11-22 14:54:14 +01:00
Devmel Apps f3705d94a2 s6-overlay-suexec fix 2022-08-16 16:53:09 +02:00
13 changed files with 473 additions and 16 deletions
+13
View File
@@ -0,0 +1,13 @@
# Changelog
## 1.2
- RF listening and states managements on callback
## 1.1
- Repository Installation
## 1.0
- Initial release
+15 -1
View File
@@ -4,8 +4,22 @@
Home Assistant Addon to control an AirSend device in a local network. Home Assistant Addon to control an AirSend device in a local network.
## Installation ## Repository Installation
[![Open your Home Assistant instance and show the add add-on repository dialog with a specific repository URL pre-filled.](https://my.home-assistant.io/badges/supervisor_add_addon_repository.svg)](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Fdevmel%2Fhass_airsend-addon)
## Manual Installation
1. Into the terminal, run `wget -q -O - https://raw.githubusercontent.com/devmel/hass_airsend-addon/master/install | bash -` 1. Into the terminal, run `wget -q -O - https://raw.githubusercontent.com/devmel/hass_airsend-addon/master/install | bash -`
OR copy the `airsend` folder into your [addon folder](https://developers.home-assistant.io/docs/creating_integration_file_structure/#where-home-assistant-looks-for-integrations). OR copy the `airsend` folder into your [addon folder](https://developers.home-assistant.io/docs/creating_integration_file_structure/#where-home-assistant-looks-for-integrations).
2. In Supervisor -> Add-on store -> Local add-ons, refresh, install and start AirSend addon 2. In Supervisor -> Add-on store -> Local add-ons, refresh, install and start AirSend addon
## Install on an external machine
1. Clone repository
2. Go to addons/airsend folder
3. In DockerFile : replace `ARG BUILD_FROM FROM $BUILD_FROM` with your machine architecture build, example : `FROM ghcr.io/home-assistant/amd64-base:3.18`
4. In callback.php : replace $BASE_HASS_API and $HASS_API_TOKEN with your home automation machine values
5. In terminal, run `docker build -t hass_airsend-addon .`
6. In terminal, run `docker run -dp 33863:33863 hass_airsend-addon`
+6 -1
View File
@@ -2,12 +2,17 @@ ARG BUILD_FROM
FROM $BUILD_FROM FROM $BUILD_FROM
# Copy data for add-on # Copy data for add-on
COPY hassapi.class.php /home
COPY callback.php /home
COPY poststate.sh /home
COPY run.sh / COPY run.sh /
RUN chmod a+x /home/poststate.sh
RUN chmod a+x /run.sh RUN chmod a+x /run.sh
# Install AirSendWebService # Install AirSendWebService
# RUN mkdir -p /home && cd /home && wget http://devmel.com/dl/AirSendWebService.tgz && (gzip -dc AirSendWebService.tgz | tar -xvzf -) && chmod -R 777 bin
RUN mkdir -p /home && cd /home && wget http://devmel.com/dl/AirSendWebService.tgz && tar -zxvf AirSendWebService.tgz && chmod -R 777 bin RUN mkdir -p /home && cd /home && wget http://devmel.com/dl/AirSendWebService.tgz && tar -zxvf AirSendWebService.tgz && chmod -R 777 bin
RUN apk add php php-curl
# Start AirSendWebService # Start AirSendWebService
CMD [ "/run.sh" ] CMD [ "/run.sh" ]
EXPOSE 33863
+9
View File
@@ -0,0 +1,9 @@
build_from:
aarch64: ghcr.io/home-assistant/aarch64-base:3.18
amd64: ghcr.io/home-assistant/amd64-base:3.18
armhf: ghcr.io/home-assistant/armhf-base:3.18
armv7: ghcr.io/home-assistant/armv7-base:3.18
i386: ghcr.io/home-assistant/i386-base:3.18
codenotary:
signer: notary@home-assistant.io
base_image: notary@home-assistant.io
+65
View File
@@ -0,0 +1,65 @@
<?php
require_once dirname(__FILE__) . "/hassapi.class.php";
$BASE_HASS_API = "http://supervisor/core/api";
$HASS_API_TOKEN = @file_get_contents('hass_api.token');
//On an external machine, replace with your ha values
//$BASE_HASS_API = "http://homeassistant.local:8123/api";
//$HASS_API_TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI3YTJjYjE2M2VjZjQ0MGM0OGUwYzdkOTc2MjM4YWY5MCIsImlhdCI6MTY2ODg5NzA4OCwiZXhwIjoxOTg0MjU3MDg4fQ.gyDg_jYbD561OdQ0IngAMga-4LE3DTsd6bEIGkITGTc';
$api = new HassAPI($BASE_HASS_API, $HASS_API_TOKEN);
if(!$api->isAuthorized()){
header("HTTP/1.1 401 Unauthorized");
trigger_error("Unauthorized", E_USER_ERROR);
die();
}
$raw = file_get_contents('php://input');
$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'])){
//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']);
}
}
}
}
}
}
}
}
?>
+9 -2
View File
@@ -1,16 +1,23 @@
name: "AirSend" name: "AirSend"
description: "AirSend Web Service" description: "AirSend Web Service (local connection, no internet, no cloud)"
version: "1.0" version: "1.2"
slug: "airsend" slug: "airsend"
url: https://github.com/devmel/hass_airsend-addon url: https://github.com/devmel/hass_airsend-addon
codenotary: notary@home-assistant.io
arch: arch:
- aarch64 - aarch64
- amd64 - amd64
- armhf - armhf
- armv7 - armv7
- i386 - i386
homeassistant_api: true
startup: services startup: services
init: false
ports: ports:
33863/tcp: null 33863/tcp: null
ports_description: ports_description:
33863/tcp: AirSendWebService communication 33863/tcp: AirSendWebService communication
options:
auto_include: true
schema:
auto_include: bool
+321
View File
@@ -0,0 +1,321 @@
<?php
class HassAPI{
var $states = null;
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()){
$fbvalue = @file_get_contents('auto_include.cfg');
$this->auto_include = filter_var($fbvalue, FILTER_VALIDATE_BOOLEAN);
$this->loadStates();
}
}
public function isAuthorized() {
if(!empty($this->BASE_HASS_API) && !empty($this->HASS_API_TOKEN)){
return true;
}
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 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);
if($this->states){
foreach ($this->states as &$value) {
$h = str_split(hash('sha256', $value['entity_id']),12)[0];
$val = intval($h, 16);
$value['entity_uid_sha256'] = $val;
}
}
}
}
private static function request($url, $data = null, $method = 'GET', $token = null) {
$result = false;
if (extension_loaded('curl') === true){
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_AUTOREFERER, true);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
if (preg_match('~^(?:DELETE|GET|POST|PUT)$~i', $method) > 0){
if (preg_match('~^(?:POST|PUT)$~i', $method) > 0){
if (is_array($data) === true){
foreach (preg_grep('~^@~', $data) as $key => $value){
$data[$key] = sprintf('@%s', rtrim(str_replace('\\', '/', realpath(ltrim($value, '@'))), '/') . (is_dir(ltrim($value, '@')) ? '/' : ''));
}
if (count($data) != count($data, COUNT_RECURSIVE)){
$data = http_build_query($data, '', '&');
}
}
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
}
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, strtoupper($method));
if (isset($token)){
$token = str_replace('"', "", $token);
$options = array(CURLOPT_HTTPHEADER => array('Content-Type: application/json' , "Authorization: Bearer ".$token));
curl_setopt_array($curl, $options);
}
$result = array();
$result['data'] = curl_exec($curl);
$result['info'] = curl_getinfo($curl);
if (curl_errno($curl)) {
$result['error'] = curl_error($curl);
}
}
curl_close($curl);
}
return $result;
}
}
?>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 22 KiB

+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/with-contenv bashio
payload=`echo $2 | base64 -d`
echo -en $payload | curl -s --data-binary @- -X POST -H "Authorization: Bearer ${SUPERVISOR_TOKEN}" -H "Content-Type: application/json" http://supervisor/core/api/states/$1
+15 -1
View File
@@ -1,13 +1,27 @@
#!/usr/bin/with-contenv bashio #!/usr/bin/with-contenv bashio
cd /home
arch="$(apk --print-arch)" arch="$(apk --print-arch)"
case "$arch" in \ case "$arch" in \
aarch64) arch='arm64' ;; \ aarch64) arch='arm64' ;; \
armhf) arch='armhf' ;; \
armv7) arch='arm' ;; \ armv7) arch='arm' ;; \
amd64) arch='x86_64' ;; \ amd64) arch='x86_64' ;; \
i386) arch='x86' ;; \ i386) arch='x86' ;; \
esac; esac;
echo "AirSendWebService arch: ${arch}"
hname="$(hostname -i)"
echo "internal_url: http://${hname}:33863/"
cd /home 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
ulimit -n 4096
./bin/unix/${arch}/AirSendWebService 99399 ./bin/unix/${arch}/AirSendWebService 99399
php -S 127.0.0.1:80 callback.php
sleep infinity sleep infinity
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 22 KiB

+1 -1
View File
@@ -45,7 +45,7 @@ if [ -n "$haPath" ]; then
cd "$haPath" || error "Could not change path to $haPath" cd "$haPath" || error "Could not change path to $haPath"
info "Downloading AirSend Home Assistant Addon" info "Downloading AirSend Home Assistant Addon"
wget "https://github.com/devmel/hass_airsend-addon/releases/download/1.0/hass_airsend-addon.zip" wget "https://github.com/devmel/hass_airsend-addon/releases/download/latest/hass_airsend-addon.zip"
if [ -d "$haPath/airsend" ]; then if [ -d "$haPath/airsend" ]; then
warn "airsend directory already exist, cleaning up..." warn "airsend directory already exist, cleaning up..."
+4
View File
@@ -0,0 +1,4 @@
# https://developers.home-assistant.io/docs/add-ons/repository#repository-configuration
name: AirSend Home Assistant add-on repository
url: 'https://github.com/devmel/hass_airsend-addon'
maintainer: Devmel Apps <apps@devmel.com>