AirSend duo

This commit is contained in:
Devmel Apps
2022-11-16 12:59:19 +01:00
parent 1513a864dc
commit bb072fd573
7 changed files with 318 additions and 305 deletions
+6 -5
View File
@@ -2,15 +2,16 @@
# AirSend Home Assistant
Component for sending radio commands (433-434Mhz) through the AirSend device.
Component for sending radio commands through the AirSend (RF433) or AirSend duo (RF433 & RF868).
## Installation
1. Add `airsend:` to your HA configuration (see configuration below).
2. Into the terminal, run `wget -q -O - https://raw.githubusercontent.com/devmel/hass_airsend/master/install | bash -`
1. Into the terminal, run `wget -q -O - https://raw.githubusercontent.com/devmel/hass_airsend/master/install | bash -`
OR copy the `airsend` folder into your [custom_components folder](https://developers.home-assistant.io/docs/creating_integration_file_structure/#where-home-assistant-looks-for-integrations).
3. To allow a local LAN connection please install and start [hass_airsend-addon](https://github.com/devmel/hass_airsend-addon).
4. Restart Home Assistant
2. To allow a local LAN connection please install and start [hass_airsend-addon](https://github.com/devmel/hass_airsend-addon).
3. Restart Home Assistant
4. Add `airsend:` to your HA configuration (see configuration below).
5. Restart Home Assistant
## Configuration
+20 -4
View File
@@ -2,17 +2,33 @@
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers import discovery
from homeassistant.const import (
CONF_DEVICES,
from homeassistant.components.hassio import (
async_get_addon_info,
)
from homeassistant.const import CONF_INTERNAL_URL
DOMAIN = "airsend"
AS_TYPE = ['switch', 'cover', 'button']
AS_TYPE = ["switch", "cover", "button"]
async def async_setup(hass: HomeAssistant, config: ConfigType):
"""Set up the AirSend component."""
if DOMAIN not in config:
return True
internalurl = ""
try:
internalurl = config[DOMAIN][CONF_INTERNAL_URL]
except KeyError:
pass
if internalurl == "":
try:
addon_info: dict = await async_get_addon_info(hass, "local_airsend")
ip = addon_info["ip_address"]
if ip:
internalurl = "http://" + str(ip) + ":33863/"
except KeyError:
pass
config[DOMAIN][CONF_INTERNAL_URL] = internalurl
for plateform in AS_TYPE:
discovery.load_platform(hass, plateform, DOMAIN, config[DOMAIN][CONF_DEVICES].copy(), config)
discovery.load_platform(hass, plateform, DOMAIN, config[DOMAIN].copy(), config)
return True
+29 -98
View File
@@ -1,84 +1,44 @@
"""AirSend buttons."""
import logging
import json
from typing import Any
from requests import get, post
from .device import Device
from homeassistant.components.button import ButtonEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.components.hassio import (
async_get_addon_discovery_info,
async_get_addon_info,
)
from homeassistant.components.button import (
ButtonEntity,
)
from . import (
DOMAIN,
)
_LOGGER = logging.getLogger(DOMAIN)
from homeassistant.const import CONF_DEVICES, CONF_INTERNAL_URL
async def async_setup_platform(hass : HomeAssistant, config : ConfigType, async_add_entities, discovery_info=None):
addons_url = ""
try:
addon_info: dict = await async_get_addon_info(hass, 'local_airsend')
ip = addon_info["ip_address"]
if ip:
addons_url = "http://"+str(ip)+":33863/"
# _LOGGER.warning("Addon '%s'", addon_info)
except:
pass
from . import DOMAIN
async def async_setup_platform(
hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None
) -> None:
if discovery_info is None:
return
for name, options in discovery_info.items():
if options['type'] == 4096 :
id = ""
apiKey = ""
spurl = ""
channel = {}
note = {"method":1,"type":0,"value": "TOGGLE"}
try:
id = options['id']
except KeyError:
pass
try:
apiKey = options['apiKey']
except KeyError:
pass
try:
spurl = options['spurl']
except KeyError:
pass
try:
channel = options['channel']
except KeyError:
pass
try:
note = options['note']
except KeyError:
pass
entity = AirSendButton(hass, name, id, options['type'], apiKey, addons_url, spurl, channel, note)
for name, options in discovery_info[CONF_DEVICES].items():
device = Device(name, options, discovery_info[CONF_INTERNAL_URL])
if device.is_button:
entity = AirSendButton(
hass,
device,
)
async_add_entities([entity])
return
class AirSendButton(ButtonEntity):
"""Representation of an AirSend Button."""
def __init__(self, hass : HomeAssistant, name : str, id: str, type : int, apikey : str, addons_url : str, spurl : str, channel : dict, note : dict):
def __init__(
self,
hass: HomeAssistant,
device: Device,
) -> None:
"""Initialize a button."""
self._name = name
self._id = id
self._type = type
self._apikey = apikey
self._addons_url = addons_url
self._spurl = spurl
self._channel = channel
self._note = note
uname = DOMAIN+name
self._device = device
uname = DOMAIN + device.name
self._unique_id = "_".join(x for x in uname)
self._state = False
@property
def unique_id(self):
@@ -97,7 +57,7 @@ class AirSendButton(ButtonEntity):
@property
def name(self):
"""Return the name of the device if any."""
return self._name
return self._device.name
@property
def extra_state_attributes(self):
@@ -109,37 +69,8 @@ class AirSendButton(ButtonEntity):
"""Return true if unable to access real state of entity."""
return True
def press(self) -> None:
def press(self, **kwargs: Any) -> None:
"""Handle the button press."""
command = "6"
note = self._note
url = "https://airsend.cloud/device/" + str(self._id) + "/command/" + command + "/"
payload = '{"wait": true, "channel":' + json.dumps(self._channel) + ', "thingnotes":{"notes":[' + json.dumps(
note) + ']}}'
self._action(url, payload, True)
def _action(self, cloud_url : str, payload : str, new_state : bool):
status_code = 404
if self._addons_url and self._spurl:
headers = {"Authorization": "Bearer "+self._spurl, "content-type": "application/json", "User-Agent": "hass_airsend"}
try:
response = post(self._addons_url+"airsend/transfer", headers=headers, data=payload, timeout=6)
status_code = 500
jdata = json.loads(response.text)
if jdata["type"] < 0x100:
status_code = response.status_code
except:
pass
if status_code != 200 and self._apikey and cloud_url:
headers = {"Authorization": "Bearer "+self._apikey, "content-type": "application/json", "User-Agent": "hass_airsend"}
try:
response = get(cloud_url, headers=headers, timeout=10)
status_code = response.status_code
except:
pass
if status_code == 200:
self._state = new_state
note = {"method": 1, "type": 0, "value": "TOGGLE"}
if self._device.transfer(note):
self.schedule_update_ha_state()
else:
_LOGGER.error("action error : "+str(status_code))
raise Exception("action error : "+str(status_code))
+54 -94
View File
@@ -1,78 +1,47 @@
"""AirSend switches."""
import logging
import json
from typing import Any
from requests import get, post
from .device import Device
from homeassistant.components.cover import CoverEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.components.hassio import (
async_get_addon_discovery_info,
async_get_addon_info,
)
from homeassistant.components.cover import (
CoverEntity,
)
from . import (
DOMAIN,
)
_LOGGER = logging.getLogger(DOMAIN)
from homeassistant.const import CONF_DEVICES, CONF_INTERNAL_URL
async def async_setup_platform(hass : HomeAssistant, config : ConfigType, async_add_entities, discovery_info=None):
addons_url = ""
try:
addon_info: dict = await async_get_addon_info(hass, 'local_airsend')
ip = addon_info["ip_address"]
if ip:
addons_url = "http://"+str(ip)+":33863/"
# _LOGGER.warning("Addon '%s'", addon_info)
except:
pass
from . import DOMAIN
async def async_setup_platform(
hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None
) -> None:
if discovery_info is None:
return
for name, options in discovery_info.items():
if options['type'] == 4098:
id = ""
apiKey = ""
spurl = ""
channel = {}
try:
id = options['id']
except KeyError:
pass
try:
apiKey = options['apiKey']
except KeyError:
pass
try:
spurl = options['spurl']
except KeyError:
pass
try:
channel = options['channel']
except KeyError:
pass
entity = AirSendCover(hass, name, id, options['type'], apiKey, addons_url, spurl, channel)
for name, options in discovery_info[CONF_DEVICES].items():
device = Device(name, options, discovery_info[CONF_INTERNAL_URL])
if device.is_cover:
entity = AirSendCover(
hass,
device,
)
async_add_entities([entity])
return
class AirSendCover(CoverEntity):
"""Representation of an AirSend Cover."""
def __init__(self, hass : HomeAssistant, name : str, id: str, type : int, apikey : str, addons_url : str, spurl : str, channel : dict):
def __init__(
self,
hass: HomeAssistant,
device: Device,
) -> None:
"""Initialize a cover device."""
self._name = name
self._id = id
self._type = type
self._apikey = apikey
self._addons_url = addons_url
self._spurl = spurl
self._channel = channel
uname = DOMAIN+name
self._device = device
uname = DOMAIN + device.name
self._unique_id = "_".join(x for x in uname)
self._state = False
self._closed = False
if device.is_cover_with_position:
self._attr_current_cover_position = 50
@property
def unique_id(self):
@@ -91,7 +60,7 @@ class AirSendCover(CoverEntity):
@property
def name(self):
"""Return the name of the device if any."""
return self._name
return self._device.name
@property
def extra_state_attributes(self):
@@ -106,51 +75,42 @@ class AirSendCover(CoverEntity):
@property
def is_closed(self):
"""Return if the cover is closed."""
return not self._state
return not self._closed
def open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
note = {"method": 1, "type": 0, "value": "UP"}
url = "https://airsend.cloud/device/"+str(self._id)+"/command/5/"
payload = '{"wait": true, "channel":'+json.dumps(self._channel)+', "thingnotes":{"notes":['+json.dumps(note)+']}}'
self._action(url, payload, False)
if self._device.transfer(note):
self._closed = False
if self._device.is_cover_with_position:
self._attr_current_cover_position = 100
self.schedule_update_ha_state()
def close_cover(self, **kwargs: Any) -> None:
"""Close cover."""
note = {"method": 1, "type": 0, "value": "DOWN"}
url = "https://airsend.cloud/device/"+str(self._id)+"/command/4/"
payload = '{"wait": true, "channel":'+json.dumps(self._channel)+', "thingnotes":{"notes":['+json.dumps(note)+']}}'
self._action(url, payload, False)
if self._device.transfer(note):
self._closed = True
if self._device.is_cover_with_position:
self._attr_current_cover_position = 0
self.schedule_update_ha_state()
def stop_cover(self, **kwargs):
"""Stop the cover."""
note = {"method": 1, "type": 0, "value": "STOP"}
url = "https://airsend.cloud/device/"+str(self._id)+"/command/3/"
payload = '{"wait": true, "channel":'+json.dumps(self._channel)+', "thingnotes":{"notes":['+json.dumps(note)+']}}'
self._action(url, payload, False)
if self._device.transfer(note):
self._closed = False
if self._device.is_cover_with_position:
self._attr_current_cover_position = 50
self.schedule_update_ha_state()
def _action(self, cloud_url : str, payload : str, new_state : bool):
status_code = 404
if self._addons_url and self._spurl:
headers = {"Authorization": "Bearer "+self._spurl, "content-type": "application/json", "User-Agent": "hass_airsend"}
try:
response = post(self._addons_url+"airsend/transfer", headers=headers, data=payload, timeout=6)
status_code = 500
jdata = json.loads(response.text)
if jdata["type"] < 0x100:
status_code = response.status_code
except:
pass
if status_code != 200 and self._apikey and cloud_url:
headers = {"Authorization": "Bearer "+self._apikey, "content-type": "application/json", "User-Agent": "hass_airsend"}
try:
response = get(cloud_url, headers=headers, timeout=10)
status_code = response.status_code
except:
pass
if status_code == 200:
self._state = new_state
def set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
position = int(kwargs["position"])
note = {"method": 1, "type": 9, "value": position}
if self._device.transfer(note):
self._attr_current_cover_position = position
self._closed = False
if self._attr_current_cover_position == 0:
self._closed = True
self.schedule_update_ha_state()
else:
_LOGGER.error("action error : "+str(status_code))
raise Exception("action error : "+str(status_code))
+172
View File
@@ -0,0 +1,172 @@
"""AirSend device."""
import logging
import json
from requests import get, post, exceptions
from . import DOMAIN
_LOGGER = logging.getLogger(DOMAIN)
class Device:
"""Representation of a Device."""
def __init__(
self,
name: str,
options,
serviceurl: str,
) -> None:
"""Initialize a device."""
self._name = name
self._serviceurl = serviceurl
self._uid = None
self._rtype = None
self._apikey = None
self._spurl = None
self._channel = {}
self._note = None
try:
self._uid = options["id"]
except KeyError:
pass
try:
self._rtype = options["type"]
except KeyError:
pass
try:
self._apikey = options["apiKey"]
except KeyError:
pass
try:
self._spurl = options["spurl"]
except KeyError:
pass
try:
self._channel = options["channel"]
except KeyError:
pass
try:
self._note = options["note"]
except KeyError:
pass
@property
def name(self) -> str:
"""Return the name."""
return self._name
@property
def is_button(self) -> bool:
"""Return if is a button."""
if self._rtype == 4096:
return True
return False
@property
def is_cover(self) -> bool:
"""Return if is a cover."""
if self._rtype in (4098, 4099):
return True
return False
@property
def is_cover_with_position(self) -> bool:
"""Return if is a cover with position."""
if self._rtype == 4099:
return True
return False
@property
def is_switch(self) -> bool:
"""Return if is a switch."""
if self._rtype == 4097:
return True
return False
def transfer(self, note) -> bool:
"""Send a command."""
status_code = 404
if self._serviceurl and self._spurl:
jnote = json.dumps(note)
if (
self._note is not None
and "method" in self._note
and "type" in self._note
and "value" in self._note
and "method" in note.keys()
and "type" in note.keys()
and "value" in note.keys()
and note["method"] == 1 and note["type"] == 0
and (note["value"] == "TOGGLE" or note["value"] == 6)
):
jnote = json.dumps(self._note)
payload = (
'{"wait": true, "channel":'
+ json.dumps(self._channel)
+ ', "thingnotes":{"notes":['
+ jnote
+ "]}}"
)
headers = {
"Authorization": "Bearer " + self._spurl,
"content-type": "application/json",
"User-Agent": "hass_airsend",
}
try:
response = post(
self._serviceurl + "airsend/transfer",
headers=headers,
data=payload,
timeout=6,
)
status_code = 500
jdata = json.loads(response.text)
if jdata["type"] < 0x100:
status_code = response.status_code
except exceptions.RequestException:
pass
if status_code != 200 and self._apikey:
action = "command"
value = 6
if (
"method" in note.keys()
and "type" in note.keys()
and "value" in note.keys()
):
if note["method"] == 1 and note["type"] == 0:
if note["value"] == "OFF":
value = 0
if note["value"] == "ON":
value = 1
if note["value"] == "STOP":
value = 3
if note["value"] == "DOWN":
value = 4
if note["value"] == "UP":
value = 5
if note["method"] == 1 and note["type"] == 9:
action = "level"
value = int(note["value"])
cloud_url = (
"https://airsend.cloud/device/"
+ str(self._uid)
+ "/"
+ action
+ "/"
+ str(value)
+ "/"
)
headers = {
"Authorization": "Bearer " + self._apikey,
"content-type": "application/json",
"User-Agent": "hass_airsend",
}
try:
response = get(cloud_url, headers=headers, timeout=10)
status_code = response.status_code
except exceptions.RequestException:
pass
if status_code == 200:
return True
_LOGGER.error("Transfer error '%s' : '%s'", self.name, status_code)
raise Exception("Transfer error " + self.name + " : " + str(status_code))
+31 -98
View File
@@ -1,82 +1,43 @@
"""AirSend switches."""
import logging
import json
from typing import Any
from requests import get, post
from .device import Device
from homeassistant.components.switch import SwitchEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.components.hassio import (
async_get_addon_discovery_info,
async_get_addon_info,
)
from homeassistant.components.switch import (
SwitchEntity,
)
from . import (
DOMAIN,
)
_LOGGER = logging.getLogger(DOMAIN)
from homeassistant.const import CONF_DEVICES, CONF_INTERNAL_URL
async def async_setup_platform(hass : HomeAssistant, config : ConfigType, async_add_entities, discovery_info=None):
addons_url = ""
try:
addon_info: dict = await async_get_addon_info(hass, 'local_airsend')
ip = addon_info["ip_address"]
if ip:
addons_url = "http://"+str(ip)+":33863/"
# _LOGGER.warning("Addon '%s'", addon_info)
except:
pass
from . import DOMAIN
async def async_setup_platform(
hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None
) -> None:
if discovery_info is None:
return
for name, options in discovery_info.items():
if options['type'] == 4097:
id = ""
apiKey = ""
spurl = ""
channel = {}
note = {"method":1,"type":0,"value": "TOGGLE"}
try:
id = options['id']
except KeyError:
pass
try:
apiKey = options['apiKey']
except KeyError:
pass
try:
spurl = options['spurl']
except KeyError:
pass
try:
channel = options['channel']
except KeyError:
pass
try:
note = options['note']
except KeyError:
pass
entity = AirSendSwitch(hass, name, id, options['type'], apiKey, addons_url, spurl, channel, note)
for name, options in discovery_info[CONF_DEVICES].items():
device = Device(name, options, discovery_info[CONF_INTERNAL_URL])
if device.is_switch:
entity = AirSendSwitch(
hass,
device,
)
async_add_entities([entity])
return
class AirSendSwitch(SwitchEntity):
"""Representation of an AirSend Switch."""
def __init__(self, hass : HomeAssistant, name : str, id: str, type : int, apikey : str, addons_url : str, spurl : str, channel : dict, note : dict):
def __init__(
self,
hass: HomeAssistant,
device: Device,
) -> None:
"""Initialize a switch or light device."""
self._name = name
self._id = id
self._type = type
self._apikey = apikey
self._addons_url = addons_url
self._spurl = spurl
self._channel = channel
self._note = note
uname = DOMAIN+name
self._device = device
uname = DOMAIN + device.name
self._unique_id = "_".join(x for x in uname)
self._state = False
@@ -97,7 +58,7 @@ class AirSendSwitch(SwitchEntity):
@property
def name(self):
"""Return the name of the device if any."""
return self._name
return self._device.name
@property
def extra_state_attributes(self):
@@ -115,42 +76,14 @@ class AirSendSwitch(SwitchEntity):
def turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
command = "1"
note = {"method": 1, "type": 0, "value": "ON"}
url = "https://airsend.cloud/device/"+str(self._id)+"/command/"+command+"/"
payload = '{"wait": true, "channel":'+json.dumps(self._channel)+', "thingnotes":{"notes":['+json.dumps(note)+']}}'
self._action(url, payload, True)
if self._device.transfer(note):
self._state = True
self.schedule_update_ha_state()
def turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
command = "0"
note = {"method": 1, "type": 0, "value": "OFF"}
url = "https://airsend.cloud/device/"+str(self._id)+"/command/"+command+"/"
payload = '{"wait": true, "channel":'+json.dumps(self._channel)+', "thingnotes":{"notes":['+json.dumps(note)+']}}'
self._action(url, payload, False)
def _action(self, cloud_url : str, payload : str, new_state : bool):
status_code = 404
if self._addons_url and self._spurl:
headers = {"Authorization": "Bearer "+self._spurl, "content-type": "application/json", "User-Agent": "hass_airsend"}
try:
response = post(self._addons_url+"airsend/transfer", headers=headers, data=payload, timeout=6)
status_code = 500
jdata = json.loads(response.text)
if jdata["type"] < 0x100:
status_code = response.status_code
except:
pass
if status_code != 200 and self._apikey and cloud_url:
headers = {"Authorization": "Bearer "+self._apikey, "content-type": "application/json", "User-Agent": "hass_airsend"}
try:
response = get(cloud_url, headers=headers, timeout=10)
status_code = response.status_code
except:
pass
if status_code == 200:
self._state = new_state
if self._device.transfer(note):
self._state = False
self.schedule_update_ha_state()
else:
_LOGGER.error("action error : "+str(status_code))
raise Exception("action error : "+str(status_code))
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 22 KiB