Compare commits
10 Commits
db63cab455
...
master
Author | SHA1 | Date | |
---|---|---|---|
1114014bc9 | |||
31dfa017cf | |||
7a2f84dc25 | |||
e4439cb176 | |||
e8d6057264 | |||
9cf9bc11d3 | |||
3f06c92abb | |||
5703fff482 | |||
8c222d05b3 | |||
4bc0e85bad |
27
README.md
27
README.md
@ -2,15 +2,15 @@
|
|||||||
|
|
||||||
An [Albert](https://albertlauncher.github.io/) extention to view and control devices in your [HomeAssistant](https://www.home-assistant.io/) instance.
|
An [Albert](https://albertlauncher.github.io/) extention to view and control devices in your [HomeAssistant](https://www.home-assistant.io/) instance.
|
||||||
|
|
||||||
This extension is heavily inspired by the home assistant extesnsion for ulauncher from [qcasey](https://github.com/qcasey/ulauncher-homeassistant).
|
This extension is heavily inspired by the home assistant extension for ulauncher from [qcasey](https://github.com/qcasey/ulauncher-homeassistant).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
To use this extension, you need the Python `requests` library:
|
To use this extension, you need the Python `requests` library:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
pip install requests
|
pip install requests
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -19,8 +19,26 @@ pip install requests
|
|||||||
You need to specify the URL and [API Key](https://developers.home-assistant.io/docs/api/rest/¦) of your Home Assistant instance in the configuration file (e.g. location: `$HOME/.config/albert/homeassistant_config.json`).
|
You need to specify the URL and [API Key](https://developers.home-assistant.io/docs/api/rest/¦) of your Home Assistant instance in the configuration file (e.g. location: `$HOME/.config/albert/homeassistant_config.json`).
|
||||||
You can generate a new long lived API Key by clicking your name in the bottom left in the Home Assistant UI.
|
You can generate a new long lived API Key by clicking your name in the bottom left in the Home Assistant UI.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"hass_url": "http://192.168.1.2:8123",
|
||||||
|
"hass_key": "s1GeOobviEbZS3OjfkYVmOQDMbCRIH0kCtisGVi2EQoKbDeqq"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The item order is based on the entity class. Lights and switches are shown first, then scenes and groups etc. You can change the order in the configuration file with `sort_order`. See the default sorting order below:
|
||||||
|
```json
|
||||||
|
{ ...
|
||||||
|
sort_order: {"light": 1, "switch": 1, "scene": 2, "group": 2, "automation": 3}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
`<trigger> <entity search>`
|
```
|
||||||
|
<trigger> <entity search>
|
||||||
|
```
|
||||||
|
|
||||||
You can see the actions by pressing the `alt` key.
|
You can see the actions by pressing the `alt` key.
|
||||||
|
|
||||||
@ -28,7 +46,6 @@ You can see the actions by pressing the `alt` key.
|
|||||||
|
|
||||||
- renew icons (.svg)
|
- renew icons (.svg)
|
||||||
- test more device classes and adjust which service to call
|
- test more device classes and adjust which service to call
|
||||||
- sort items: name matching first
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
74
__init__.py
74
__init__.py
@ -18,11 +18,10 @@ __py_deps__ =["requests"]
|
|||||||
|
|
||||||
|
|
||||||
# global variables
|
# global variables
|
||||||
|
config = {}
|
||||||
configurationFileName = "homeassistant_config.json"
|
configurationFileName = "homeassistant_config.json"
|
||||||
configuration_directory = os.path.join(configLocation())
|
configuration_directory = os.path.join(configLocation())
|
||||||
configuration_file = os.path.join(configuration_directory, configurationFileName)
|
configuration_file = os.path.join(configuration_directory, configurationFileName)
|
||||||
config = {}
|
|
||||||
#config["sort_order"] = [["light", "switch"], ["scene", "group"], ["automation"]]
|
|
||||||
config["sort_order"] = {"light": 1, "switch": 1, "scene": 2, "group": 2, "automation": 3}
|
config["sort_order"] = {"light": 1, "switch": 1, "scene": 2, "group": 2, "automation": 3}
|
||||||
|
|
||||||
icon_files = {
|
icon_files = {
|
||||||
@ -35,7 +34,7 @@ icon_files = {
|
|||||||
"switch": "icons/switch.png",
|
"switch": "icons/switch.png",
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle_types = ["light", "switch", "automation", "group", "input_boolean", "climate", "camera"]
|
toggle_types = ["light", "switch", "automation", "group", "script", "input_boolean", "climate", "camera"]
|
||||||
on_off_types = ["scene", "media_player"] # use turn_{on,off} instead of toggle
|
on_off_types = ["scene", "media_player"] # use turn_{on,off} instead of toggle
|
||||||
|
|
||||||
|
|
||||||
@ -59,6 +58,15 @@ def initialize():
|
|||||||
except OSError:
|
except OSError:
|
||||||
critical("There was an error making the directory: %s" % configuration_directory)
|
critical("There was an error making the directory: %s" % configuration_directory)
|
||||||
|
|
||||||
|
# Set up HASS state query
|
||||||
|
config["hass_url"] = config["hass_url"].strip("/")
|
||||||
|
config["state_query"] = config["hass_url"] + "/api/states"
|
||||||
|
config["headers"]= {
|
||||||
|
"Authorization": "Bearer " + config["hass_key"],
|
||||||
|
"content-type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set up sort_order
|
||||||
config["sort_order"]["."] = 999 # Wildcard for sorting
|
config["sort_order"]["."] = 999 # Wildcard for sorting
|
||||||
config["sort_order"] = RegexDict(config["sort_order"])
|
config["sort_order"] = RegexDict(config["sort_order"])
|
||||||
|
|
||||||
@ -91,8 +99,6 @@ def handleQuery(query):
|
|||||||
text="Invalid Home Assistant URL",
|
text="Invalid Home Assistant URL",
|
||||||
subtext=config["hass_url"])
|
subtext=config["hass_url"])
|
||||||
|
|
||||||
# Trim hass URL
|
|
||||||
config["hass_url"] = config["hass_url"].strip("/")
|
|
||||||
|
|
||||||
if "hass_key" not in config or not config["hass_key"]:
|
if "hass_key" not in config or not config["hass_key"]:
|
||||||
return Item(id=__title__,
|
return Item(id=__title__,
|
||||||
@ -113,39 +119,31 @@ def showEntities(query):
|
|||||||
return Item(id=__title__,
|
return Item(id=__title__,
|
||||||
icon=os.path.dirname(__file__) + "/" + icon_files["logo"],
|
icon=os.path.dirname(__file__) + "/" + icon_files["logo"],
|
||||||
text=__title__,
|
text=__title__,
|
||||||
subtext="Enter a query to control your Home Assistant",
|
subtext="Enter the name of the entity which you want to control",
|
||||||
actions=[UrlAction("Open in Browser", config["hass_url"])])
|
actions=[UrlAction("Open in Browser", config["hass_url"])])
|
||||||
|
|
||||||
action_word = query.string.split()[0].lower().strip()
|
entity_query_list = query.string.split()
|
||||||
#is_action_word = action_word in action_words
|
|
||||||
entity_query_list = query.string.split()#[1:] if is_action_word else query.string.split()
|
|
||||||
|
|
||||||
if not entity_query_list:
|
|
||||||
return Item(id=__title__,
|
|
||||||
icon=os.path.dirname(__file__) + "/" + icon_files["logo"],
|
|
||||||
text="Enter the name of the entity which you want to control",
|
|
||||||
subtext="Open Home Assistant in Browser",
|
|
||||||
actions=[UrlAction("Open in Browser", config["hass_url"])])
|
|
||||||
|
|
||||||
|
|
||||||
# Set up HASS state query
|
|
||||||
state_query = config["hass_url"] + "/api/states"
|
|
||||||
headers = {
|
|
||||||
"Authorization": "Bearer " + config["hass_key"],
|
|
||||||
"content-type": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
# Query entities from HASS
|
||||||
try:
|
try:
|
||||||
response = requests.get(state_query, headers=headers)
|
response = requests.get(config["state_query"], headers=config["headers"])
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except requests.exceptions.RequestException as error:
|
except requests.exceptions.RequestException as error:
|
||||||
|
critical(str(error))
|
||||||
return Item(id=__title__,
|
return Item(id=__title__,
|
||||||
icon=os.path.dirname(__file__) + "/" + icon_files["logo"],
|
icon=os.path.dirname(__file__) + "/" + icon_files["logo"],
|
||||||
text="Error while getting entity states from Home Assistant",
|
text="Error while getting entity states from Home Assistant",
|
||||||
subtext=str(error))
|
subtext=str(error))
|
||||||
|
|
||||||
# Sort entries
|
# Sort entries by class
|
||||||
entities = sorted(response.json(), key=lambda e: config["sort_order"].get_matching(e["entity_id"].split(".")[0]))
|
entities = sorted(response.json(), key=lambda e:
|
||||||
|
config["sort_order"].get_matching(e["entity_id"].split(".")[0]))
|
||||||
|
|
||||||
|
# Sort entries by query matching
|
||||||
|
entities = sorted(entities, key=lambda e:
|
||||||
|
RegexDict({query.string.lower() + ".": 0, ".": 1})
|
||||||
|
.get_matching(e["attributes"]["friendly_name"].lower()
|
||||||
|
if "friendly_name" in e["attributes"] else e["entity_id"]))
|
||||||
|
|
||||||
# Parse all entities and states
|
# Parse all entities and states
|
||||||
for entity in entities:
|
for entity in entities:
|
||||||
@ -187,8 +185,6 @@ def showEntities(query):
|
|||||||
data = {
|
data = {
|
||||||
"endpoint": "{}/api/services/".format(config["hass_url"]),
|
"endpoint": "{}/api/services/".format(config["hass_url"]),
|
||||||
"service_data": {"entity_id": entity["entity_id"]},
|
"service_data": {"entity_id": entity["entity_id"]},
|
||||||
"hass_key": config["hass_key"],
|
|
||||||
"headers": headers,
|
|
||||||
}
|
}
|
||||||
state = entity["state"]
|
state = entity["state"]
|
||||||
state_colored = "<font color=\"{}\">{}</font>".format(
|
state_colored = "<font color=\"{}\">{}</font>".format(
|
||||||
@ -196,6 +192,13 @@ def showEntities(query):
|
|||||||
"Red" if entity["state"] == "off" else "",
|
"Red" if entity["state"] == "off" else "",
|
||||||
entity["state"].capitalize()
|
entity["state"].capitalize()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if state == "unavailable" and "unavailable" not in entity:
|
||||||
|
entity["unavailable"] = True
|
||||||
|
entities.append(entity)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Build item for list
|
||||||
item = Item(id=__title__,
|
item = Item(id=__title__,
|
||||||
icon=entity_icon,
|
icon=entity_icon,
|
||||||
text=entity["attributes"]["friendly_name"],
|
text=entity["attributes"]["friendly_name"],
|
||||||
@ -203,6 +206,7 @@ def showEntities(query):
|
|||||||
subtext="%s | %s" % (state_colored, entity_class.capitalize())
|
subtext="%s | %s" % (state_colored, entity_class.capitalize())
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Add actions depending on class
|
||||||
if entity_class in toggle_types:
|
if entity_class in toggle_types:
|
||||||
item.addAction(FuncAction("Toggle", lambda d=data: sendCommand(d, "homeassistant/toggle")))
|
item.addAction(FuncAction("Toggle", lambda d=data: sendCommand(d, "homeassistant/toggle")))
|
||||||
|
|
||||||
@ -212,13 +216,16 @@ def showEntities(query):
|
|||||||
|
|
||||||
if entity_class == "cover":
|
if entity_class == "cover":
|
||||||
open_or_close = "open" if state != "open" else "close"
|
open_or_close = "open" if state != "open" else "close"
|
||||||
item.addAction(FuncAction("%s Cover" % (open_or_close.capitalize()), lambda d=data: sendCommand(d, "homeassistant/cover/%s_cover") % (open_or_close)))
|
item.addAction(FuncAction("%s Cover" % (open_or_close.capitalize()), lambda d=data: sendCommand(d, "cover/%s_cover") % (open_or_close)))
|
||||||
|
|
||||||
|
if entity_class == "automation":
|
||||||
|
item.addAction(FuncAction("Trigger", lambda d=data: sendCommand(d, "automation/trigger")))
|
||||||
|
|
||||||
item.addAction(ClipAction("Copy ID", entity["entity_id"]))
|
item.addAction(ClipAction("Copy ID", entity["entity_id"]))
|
||||||
|
|
||||||
results.append(item)
|
results.append(item)
|
||||||
|
|
||||||
# no entity found
|
# No entity found
|
||||||
if len(results) == 0:
|
if len(results) == 0:
|
||||||
results.append(
|
results.append(
|
||||||
Item(id=__title__,
|
Item(id=__title__,
|
||||||
@ -235,19 +242,18 @@ def sendCommand(data, service):
|
|||||||
data["endpoint"] += service
|
data["endpoint"] += service
|
||||||
debug(__title__ + ": Sending command \"" + service + "\" to " + data["service_data"]["entity_id"])
|
debug(__title__ + ": Sending command \"" + service + "\" to " + data["service_data"]["entity_id"])
|
||||||
|
|
||||||
|
|
||||||
# Make POST request to HA service
|
# Make POST request to HA service
|
||||||
try:
|
try:
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
data["endpoint"],
|
data["endpoint"],
|
||||||
data=json.dumps(data["service_data"]),
|
data=json.dumps(data["service_data"]),
|
||||||
headers=data["headers"],
|
headers=config["headers"],
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except requests.exceptions.RequestException as error:
|
except requests.exceptions.RequestException as error:
|
||||||
warning("Error while sending command to Home Assistant:\n%s" % (str(error)))
|
warning("Error while sending command to Home Assistant:\n%s" % (str(error)))
|
||||||
|
|
||||||
# New class to match dict keys with regex
|
# New dictionary class to match keys with regex
|
||||||
class RegexDict(dict):
|
class RegexDict(dict):
|
||||||
def get_matching(self, event):
|
def get_matching(self, event):
|
||||||
return next(self[key] for key in self if re.match(key, event))
|
return next(self[key] for key in self if re.match(key, event))
|
||||||
|
BIN
ha_demo.mp4
Normal file
BIN
ha_demo.mp4
Normal file
Binary file not shown.
Reference in New Issue
Block a user