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 # 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 ## Installation
1. Add `airsend:` to your HA configuration (see configuration below). 1. Into the terminal, run `wget -q -O - https://raw.githubusercontent.com/devmel/hass_airsend/master/install | bash -`
2. 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). 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). 2. To allow a local LAN connection please install and start [hass_airsend-addon](https://github.com/devmel/hass_airsend-addon).
4. Restart Home Assistant 3. Restart Home Assistant
4. Add `airsend:` to your HA configuration (see configuration below).
5. Restart Home Assistant
## Configuration ## Configuration
+20 -4
View File
@@ -2,17 +2,33 @@
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.const import ( from homeassistant.components.hassio import (
CONF_DEVICES, async_get_addon_info,
) )
from homeassistant.const import CONF_INTERNAL_URL
DOMAIN = "airsend" DOMAIN = "airsend"
AS_TYPE = ['switch', 'cover', 'button'] AS_TYPE = ["switch", "cover", "button"]
async def async_setup(hass: HomeAssistant, config: ConfigType): async def async_setup(hass: HomeAssistant, config: ConfigType):
"""Set up the AirSend component.""" """Set up the AirSend component."""
if DOMAIN not in config: if DOMAIN not in config:
return True 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: 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 return True
+29 -98
View File
@@ -1,84 +1,44 @@
"""AirSend buttons.""" """AirSend buttons."""
import logging
import json
from typing import Any 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.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType 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): from . import DOMAIN
addons_url = ""
try:
addon_info: dict = await async_get_addon_info(hass, 'local_airsend') async def async_setup_platform(
ip = addon_info["ip_address"] hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None
if ip: ) -> None:
addons_url = "http://"+str(ip)+":33863/"
# _LOGGER.warning("Addon '%s'", addon_info)
except:
pass
if discovery_info is None: if discovery_info is None:
return return
for name, options in discovery_info.items(): for name, options in discovery_info[CONF_DEVICES].items():
if options['type'] == 4096 : device = Device(name, options, discovery_info[CONF_INTERNAL_URL])
id = "" if device.is_button:
apiKey = "" entity = AirSendButton(
spurl = "" hass,
channel = {} device,
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)
async_add_entities([entity]) async_add_entities([entity])
return
class AirSendButton(ButtonEntity): class AirSendButton(ButtonEntity):
"""Representation of an AirSend Button.""" """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.""" """Initialize a button."""
self._name = name self._device = device
self._id = id uname = DOMAIN + device.name
self._type = type
self._apikey = apikey
self._addons_url = addons_url
self._spurl = spurl
self._channel = channel
self._note = note
uname = DOMAIN+name
self._unique_id = "_".join(x for x in uname) self._unique_id = "_".join(x for x in uname)
self._state = False
@property @property
def unique_id(self): def unique_id(self):
@@ -97,7 +57,7 @@ class AirSendButton(ButtonEntity):
@property @property
def name(self): def name(self):
"""Return the name of the device if any.""" """Return the name of the device if any."""
return self._name return self._device.name
@property @property
def extra_state_attributes(self): def extra_state_attributes(self):
@@ -109,37 +69,8 @@ class AirSendButton(ButtonEntity):
"""Return true if unable to access real state of entity.""" """Return true if unable to access real state of entity."""
return True return True
def press(self) -> None: def press(self, **kwargs: Any) -> None:
"""Handle the button press.""" """Handle the button press."""
command = "6" note = {"method": 1, "type": 0, "value": "TOGGLE"}
note = self._note if self._device.transfer(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
self.schedule_update_ha_state() 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.""" """AirSend switches."""
import logging
import json
from typing import Any 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.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType 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): from . import DOMAIN
addons_url = ""
try:
addon_info: dict = await async_get_addon_info(hass, 'local_airsend') async def async_setup_platform(
ip = addon_info["ip_address"] hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None
if ip: ) -> None:
addons_url = "http://"+str(ip)+":33863/"
# _LOGGER.warning("Addon '%s'", addon_info)
except:
pass
if discovery_info is None: if discovery_info is None:
return return
for name, options in discovery_info.items(): for name, options in discovery_info[CONF_DEVICES].items():
if options['type'] == 4098: device = Device(name, options, discovery_info[CONF_INTERNAL_URL])
id = "" if device.is_cover:
apiKey = "" entity = AirSendCover(
spurl = "" hass,
channel = {} device,
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)
async_add_entities([entity]) async_add_entities([entity])
return
class AirSendCover(CoverEntity): class AirSendCover(CoverEntity):
"""Representation of an AirSend Cover.""" """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.""" """Initialize a cover device."""
self._name = name self._device = device
self._id = id uname = DOMAIN + device.name
self._type = type
self._apikey = apikey
self._addons_url = addons_url
self._spurl = spurl
self._channel = channel
uname = DOMAIN+name
self._unique_id = "_".join(x for x in uname) 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 @property
def unique_id(self): def unique_id(self):
@@ -91,7 +60,7 @@ class AirSendCover(CoverEntity):
@property @property
def name(self): def name(self):
"""Return the name of the device if any.""" """Return the name of the device if any."""
return self._name return self._device.name
@property @property
def extra_state_attributes(self): def extra_state_attributes(self):
@@ -106,51 +75,42 @@ class AirSendCover(CoverEntity):
@property @property
def is_closed(self): def is_closed(self):
"""Return if the cover is closed.""" """Return if the cover is closed."""
return not self._state return not self._closed
def open_cover(self, **kwargs: Any) -> None: def open_cover(self, **kwargs: Any) -> None:
"""Open the cover.""" """Open the cover."""
note = {"method": 1, "type": 0, "value": "UP"} note = {"method": 1, "type": 0, "value": "UP"}
url = "https://airsend.cloud/device/"+str(self._id)+"/command/5/" if self._device.transfer(note):
payload = '{"wait": true, "channel":'+json.dumps(self._channel)+', "thingnotes":{"notes":['+json.dumps(note)+']}}' self._closed = False
self._action(url, payload, 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: def close_cover(self, **kwargs: Any) -> None:
"""Close cover.""" """Close cover."""
note = {"method": 1, "type": 0, "value": "DOWN"} note = {"method": 1, "type": 0, "value": "DOWN"}
url = "https://airsend.cloud/device/"+str(self._id)+"/command/4/" if self._device.transfer(note):
payload = '{"wait": true, "channel":'+json.dumps(self._channel)+', "thingnotes":{"notes":['+json.dumps(note)+']}}' self._closed = True
self._action(url, payload, False) if self._device.is_cover_with_position:
self._attr_current_cover_position = 0
self.schedule_update_ha_state()
def stop_cover(self, **kwargs): def stop_cover(self, **kwargs):
"""Stop the cover.""" """Stop the cover."""
note = {"method": 1, "type": 0, "value": "STOP"} note = {"method": 1, "type": 0, "value": "STOP"}
url = "https://airsend.cloud/device/"+str(self._id)+"/command/3/" if self._device.transfer(note):
payload = '{"wait": true, "channel":'+json.dumps(self._channel)+', "thingnotes":{"notes":['+json.dumps(note)+']}}' self._closed = False
self._action(url, payload, 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): def set_cover_position(self, **kwargs):
status_code = 404 """Move the cover to a specific position."""
if self._addons_url and self._spurl: position = int(kwargs["position"])
headers = {"Authorization": "Bearer "+self._spurl, "content-type": "application/json", "User-Agent": "hass_airsend"} note = {"method": 1, "type": 9, "value": position}
try: if self._device.transfer(note):
response = post(self._addons_url+"airsend/transfer", headers=headers, data=payload, timeout=6) self._attr_current_cover_position = position
status_code = 500 self._closed = False
jdata = json.loads(response.text) if self._attr_current_cover_position == 0:
if jdata["type"] < 0x100: self._closed = True
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
self.schedule_update_ha_state() 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.""" """AirSend switches."""
import logging
import json
from typing import Any 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.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType 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): from . import DOMAIN
addons_url = ""
try:
addon_info: dict = await async_get_addon_info(hass, 'local_airsend') async def async_setup_platform(
ip = addon_info["ip_address"] hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None
if ip: ) -> None:
addons_url = "http://"+str(ip)+":33863/"
# _LOGGER.warning("Addon '%s'", addon_info)
except:
pass
if discovery_info is None: if discovery_info is None:
return return
for name, options in discovery_info.items(): for name, options in discovery_info[CONF_DEVICES].items():
if options['type'] == 4097: device = Device(name, options, discovery_info[CONF_INTERNAL_URL])
id = "" if device.is_switch:
apiKey = "" entity = AirSendSwitch(
spurl = "" hass,
channel = {} device,
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)
async_add_entities([entity]) async_add_entities([entity])
return
class AirSendSwitch(SwitchEntity): class AirSendSwitch(SwitchEntity):
"""Representation of an AirSend Switch.""" """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.""" """Initialize a switch or light device."""
self._name = name self._device = device
self._id = id uname = DOMAIN + device.name
self._type = type
self._apikey = apikey
self._addons_url = addons_url
self._spurl = spurl
self._channel = channel
self._note = note
uname = DOMAIN+name
self._unique_id = "_".join(x for x in uname) self._unique_id = "_".join(x for x in uname)
self._state = False self._state = False
@@ -97,7 +58,7 @@ class AirSendSwitch(SwitchEntity):
@property @property
def name(self): def name(self):
"""Return the name of the device if any.""" """Return the name of the device if any."""
return self._name return self._device.name
@property @property
def extra_state_attributes(self): def extra_state_attributes(self):
@@ -115,42 +76,14 @@ class AirSendSwitch(SwitchEntity):
def turn_on(self, **kwargs: Any) -> None: def turn_on(self, **kwargs: Any) -> None:
"""Turn the device on.""" """Turn the device on."""
command = "1"
note = {"method": 1, "type": 0, "value": "ON"} note = {"method": 1, "type": 0, "value": "ON"}
url = "https://airsend.cloud/device/"+str(self._id)+"/command/"+command+"/" if self._device.transfer(note):
payload = '{"wait": true, "channel":'+json.dumps(self._channel)+', "thingnotes":{"notes":['+json.dumps(note)+']}}' self._state = True
self._action(url, payload, True) self.schedule_update_ha_state()
def turn_off(self, **kwargs: Any) -> None: def turn_off(self, **kwargs: Any) -> None:
"""Turn the device off.""" """Turn the device off."""
command = "0"
note = {"method": 1, "type": 0, "value": "OFF"} note = {"method": 1, "type": 0, "value": "OFF"}
url = "https://airsend.cloud/device/"+str(self._id)+"/command/"+command+"/" if self._device.transfer(note):
payload = '{"wait": true, "channel":'+json.dumps(self._channel)+', "thingnotes":{"notes":['+json.dumps(note)+']}}' self._state = False
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
self.schedule_update_ha_state() 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