Skip to content

Commit 90888bf

Browse files
authored
Merge pull request #23 from dcapslock/2.3.5
2.3.5
2 parents 8d02053 + 0566f22 commit 90888bf

File tree

20 files changed

+457
-113
lines changed

20 files changed

+457
-113
lines changed

README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,9 @@ The Phoenix fork of Browser Mod 2 starts with Version 2.3.4. The version was mad
2323
To use the Phoenix Versoin follow these steps:
2424

2525
1. Uninstall the current HACS version (browser_mod)
26-
3. Add ```https://github.com/dcapslock/hass-browser_mod``` as a [custom HACS reporistory](https://www.hacs.xyz/docs/faq/custom_repositories/) (Integration)
27-
4. Download the latest Phoenix version (browser_mod_phx)
28-
5. Check dashboard resources for any old reference to broswer_mod.js (e.g. /browser_mod.js?automatically-added&2.3.3). Remove if present.
29-
6. Restart Home Assistant
26+
2. Add ```https://github.com/dcapslock/hass-browser_mod``` as a [custom HACS reporistory](https://www.hacs.xyz/docs/faq/custom_repositories/) (Integration)
27+
3. Download the latest Phoenix version (browser_mod_phx). As of v2.3.5, dashboard resource will be correctly updated with latest version.
28+
4. Restart Home Assistant
3029

3130
# Installation instructions
3231

custom_components/browser_mod/browser.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ def delete(self, hass):
166166
self.entities = {}
167167

168168
device = dr.async_get_device({(DOMAIN, self.browserID)})
169-
dr.async_remove_device(device.id)
169+
hass.add_job(removeDevice, hass, self.browserID, device.id)
170170

171171
@property
172172
def connection(self):
@@ -201,11 +201,41 @@ def getBrowser(hass, browserID, *, create=True):
201201

202202
def deleteBrowser(hass, browserID):
203203
"""Delete a browser by BrowserID."""
204+
204205
browsers = hass.data[DOMAIN][DATA_BROWSERS]
206+
205207
if browserID in browsers:
206208
browsers[browserID].delete(hass)
207209
del browsers[browserID]
208-
210+
else:
211+
# Non-reporting Browser
212+
dr = device_registry.async_get(hass)
213+
er = entity_registry.async_get(hass)
214+
_LOGGER.debug("deleteBrowser: Removing non-reporting browser %s", browserID)
215+
216+
dev = dr.async_get_device({(DOMAIN, browserID)})
217+
if dev:
218+
# Remove all entities associated with the device
219+
for entity in entity_registry.async_entries_for_device(er, dev.id, include_disabled_entities=True):
220+
if entity.platform == DOMAIN:
221+
_LOGGER.debug("deleteBrowser: Removing entity %s for browser %s", entity.entity_id, browserID)
222+
er.async_remove(entity.entity_id)
223+
224+
hass.add_job(removeDevice, hass, browserID, dev.id)
225+
226+
def deleteBrowsers(hass, browsers_include, browsers_exclude):
227+
"""Delete browsers by list of browsers to include and exclude."""
228+
for browserID in browsers_include:
229+
_LOGGER.debug("deleteBrowsers: Deleting browser %s (included)", browserID)
230+
deleteBrowser(hass, browserID)
231+
232+
if browsers_exclude:
233+
dr = device_registry.async_get(hass)
234+
devices = [dev for dev in dr.devices.data.values() if any(identifier for identifier in dev.identifiers if identifier[0] == DOMAIN)]
235+
for dev in devices:
236+
if dev.identifiers and list(dev.identifiers)[0][1] not in browsers_exclude:
237+
_LOGGER.debug("deleteBrowsers: Deleting browser %s (not excluded)", list(dev.identifiers)[0][1])
238+
deleteBrowser(hass, list(dev.identifiers)[0][1])
209239

210240
def getBrowserByConnection(hass, connection):
211241
"""Get the browser that has a given connection open."""
@@ -214,3 +244,14 @@ def getBrowserByConnection(hass, connection):
214244
for k, v in browsers.items():
215245
if any([c[0] == connection for c in v.connection]):
216246
return v
247+
248+
@callback
249+
def removeDevice(hass, browserID, deviceID):
250+
"""Remove a device by browserID."""
251+
dr = device_registry.async_get(hass)
252+
253+
# Remove the device from the registry
254+
dev = dr.async_get(deviceID)
255+
if dev:
256+
_LOGGER.debug("removeDevice: Removing device %s (%s)", dev.id, browserID)
257+
dr.async_remove_device(dev.id)

custom_components/browser_mod/browser_mod.js

Lines changed: 27 additions & 27 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

custom_components/browser_mod/browser_mod_panel.js

Lines changed: 8 additions & 8 deletions
Large diffs are not rendered by default.

custom_components/browser_mod/const.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
WS_RECALL_ID = f"{WS_ROOT}/recall_id"
1919
WS_LOG = f"{WS_ROOT}/log"
2020

21-
BROWSER_MOD_SERVICES = [
21+
BROWSER_MOD_BROWSER_SERVICES = [
2222
"sequence",
2323
"delay",
2424
"popup",
@@ -31,3 +31,7 @@
3131
"console",
3232
"javascript",
3333
]
34+
35+
BROWSER_MOD_COMPONENT_SERVICES = [
36+
"deregister_browser",
37+
]

custom_components/browser_mod/manifest.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
"frontend",
1111
"lovelace"
1212
],
13-
"documentation": "https://github.com/dcapslock/hass-browser_mod/blob/2.3.4/README.md",
13+
"documentation": "https://github.com/dcapslock/hass-browser_mod/blob/master/README.md",
1414
"iot_class": "local_push",
1515
"requirements": [],
16-
"version": "2.3.4"
16+
"version": "2.3.5"
1717
}

custom_components/browser_mod/mod_view.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from homeassistant.core import HomeAssistant
33
from homeassistant.components.frontend import add_extra_js_url, async_register_built_in_panel
44
from homeassistant.components.http import StaticPathConfig
5+
from homeassistant.components.lovelace.resources import ResourceStorageCollection
56

67
from .const import FRONTEND_SCRIPT_URL, SETTINGS_PANEL_URL
78

@@ -57,6 +58,7 @@ async def async_setup_view(hass: HomeAssistant):
5758

5859
# Also load Browser Mod as a lovelace resource so it's accessible to Cast
5960
resources = hass.data["lovelace"].resources
61+
resourceUrl = FRONTEND_SCRIPT_URL + "?automatically-added" + "&" + version
6062
if resources:
6163
if not resources.loaded:
6264
await resources.async_load()
@@ -66,6 +68,19 @@ async def async_setup_view(hass: HomeAssistant):
6668
for r in resources.async_items():
6769
if r["url"].startswith(FRONTEND_SCRIPT_URL):
6870
frontend_added = True
71+
if not r["url"].endswith(version):
72+
if isinstance(resources, ResourceStorageCollection):
73+
await resources.async_update_item(
74+
r["id"],
75+
{
76+
"res_type": "module",
77+
"url": resourceUrl
78+
}
79+
)
80+
else:
81+
# not the best solution, but what else can we do
82+
r["url"] = resourceUrl
83+
6984
continue
7085

7186
# While going through the resources, also preload card-mod if it is found
@@ -77,7 +92,7 @@ async def async_setup_view(hass: HomeAssistant):
7792
await resources.async_create_item(
7893
{
7994
"res_type": "module",
80-
"url": FRONTEND_SCRIPT_URL + "?automatically-added" + "&" + version,
95+
"url": resourceUrl,
8196
}
8297
)
8398
elif getattr(resources, "data", None) and getattr(
@@ -86,7 +101,7 @@ async def async_setup_view(hass: HomeAssistant):
86101
resources.data.append(
87102
{
88103
"type": "module",
89-
"url": FRONTEND_SCRIPT_URL + "?automatically-added" + "&" + version,
104+
"url": resourceUrl,
90105

91106
}
92-
)
107+
)
Lines changed: 114 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,51 @@
11
import logging
22

3-
from homeassistant.helpers import device_registry
3+
from homeassistant.helpers import device_registry, template
4+
from homeassistant.util import dt as dt_util
5+
from homeassistant.exceptions import ServiceValidationError
46

57
from .const import (
6-
BROWSER_MOD_SERVICES,
8+
BROWSER_MOD_BROWSER_SERVICES,
9+
BROWSER_MOD_COMPONENT_SERVICES,
710
DOMAIN,
811
DATA_BROWSERS,
12+
DATA_STORE,
913
)
1014

15+
from .browser import deleteBrowsers
16+
1117
_LOGGER = logging.getLogger(__name__)
1218

1319

1420
async def async_setup_services(hass):
15-
def call_service(service, targets, data):
16-
17-
browsers = hass.data[DOMAIN][DATA_BROWSERS]
18-
19-
# If no targets were specified, send to all browsers
20-
if targets is None:
21-
targets = browsers.keys()
22-
23-
for target in targets:
24-
if target not in browsers:
25-
continue
26-
browser = browsers[target]
27-
hass.create_task(browser.send(service, **data))
28-
29-
def handle_service(call):
30-
service = call.service
31-
data = {**call.data}
32-
33-
browsers = data.pop("browser_id", [])
21+
def get_browser_ids(data, selectorSuffix=""):
22+
browserIDSelector = "browser_id"
23+
deviceIDSelector = "device_id"
24+
areaIDSelector = "area_id"
25+
if selectorSuffix:
26+
browserIDSelector += f"_{selectorSuffix}"
27+
deviceIDSelector += f"_{selectorSuffix}"
28+
areaIDSelector += f"_{selectorSuffix}"
29+
30+
browsers = data.pop(browserIDSelector, [])
3431
if isinstance(browsers, str):
3532
browsers = [browsers]
3633

3734
# Support service targets
38-
browsers.extend(data.pop("device_id", []))
35+
deviceIDs = data.pop(deviceIDSelector, [])
36+
if isinstance(deviceIDs, str):
37+
deviceIDs = [deviceIDs]
38+
browsers.extend(deviceIDs)
3939
dr = device_registry.async_get(hass)
40-
for area in data.pop("area_id", []):
40+
areaIDs = data.pop(areaIDSelector, [])
41+
if isinstance(areaIDs, str):
42+
areaIDs = [areaIDs]
43+
for area in areaIDs:
4144
for dev in device_registry.async_entries_for_area(dr, area):
42-
browsers.append(list(dev.identifiers)[0][1])
45+
for identifier in dev.identifiers:
46+
if identifier[0] == DOMAIN:
47+
browsers.append(identifier[1])
48+
break
4349

4450
browserIDs = None
4551
if len(browsers):
@@ -52,8 +58,89 @@ def handle_service(call):
5258
browserIDs.add(br)
5359

5460
browserIDs = set(browserIDs)
61+
62+
return browserIDs
63+
64+
def call_service(service, targets, data):
65+
browserTargets = targets["browsers"]
66+
userTargets = targets["users"]
67+
68+
browsers = hass.data[DOMAIN][DATA_BROWSERS]
69+
70+
# If no targets were specified, send to all browsers
71+
if browserTargets is None and userTargets is None:
72+
browserTargets = browsers.keys()
73+
elif browserTargets is None and userTargets is not None:
74+
browserTargets = []
75+
76+
if userTargets is not None and len(userTargets):
77+
for userId in userTargets:
78+
for key, browser in browsers.items():
79+
if browser.data.get("browser", {}).get("userData", {}).get("id") == userId:
80+
browserTargets.append(key)
81+
82+
for browserTarget in browserTargets:
83+
if browserTarget not in browsers:
84+
continue
85+
browser = browsers[browserTarget]
86+
hass.create_task(browser.send(service, **data))
87+
88+
def handle_browser_service(call):
89+
service = call.service
90+
data = {**call.data}
91+
92+
browserIDs = get_browser_ids(data)
93+
94+
# Support User Targets
95+
users = data.pop("user_id", [])
96+
if isinstance(users, str):
97+
users = [users]
98+
99+
userIDs = None
100+
if len(users):
101+
userIDs = set()
102+
for user in users:
103+
userId = template.state_attr(hass, user, "user_id")
104+
if userId:
105+
userIDs.add(userId)
106+
else:
107+
userIDs.add(user)
108+
109+
targets = {"browsers": browserIDs, "users": userIDs}
110+
111+
call_service(service, targets, data)
112+
113+
# Hanldler for server browser_mod.deregister_browser
114+
async def deregister_browser(call):
115+
data = {**call.data}
116+
117+
browserIDs = get_browser_ids(data)
118+
if browserIDs is None:
119+
browserIDs = set()
120+
121+
excludedBrowserIDs = get_browser_ids(data, "exclude")
122+
if excludedBrowserIDs is None:
123+
excludedBrowserIDs = set()
124+
125+
if not browserIDs and not excludedBrowserIDs:
126+
raise ServiceValidationError(
127+
"No browsers to include or exclude"
128+
)
129+
130+
_LOGGER.debug("browser_mod.deregister_browser: included: %s", browserIDs)
131+
_LOGGER.debug("browser_mod.deregister_browser: excluded: %s", excludedBrowserIDs)
132+
deleteBrowsers(hass, browserIDs, excludedBrowserIDs)
55133

56-
call_service(service, browserIDs, data)
134+
store = hass.data[DOMAIN][DATA_STORE]
135+
await store.cleanup(browserIDs, excludedBrowserIDs)
136+
137+
for service in BROWSER_MOD_BROWSER_SERVICES:
138+
hass.services.async_register(DOMAIN, service, handle_browser_service)
57139

58-
for service in BROWSER_MOD_SERVICES:
59-
hass.services.async_register(DOMAIN, service, handle_service)
140+
handlerFunctions = locals()
141+
for service in BROWSER_MOD_COMPONENT_SERVICES:
142+
if service in handlerFunctions:
143+
hass.services.async_register(DOMAIN, service, handlerFunctions[service])
144+
else:
145+
_LOGGER.error("Component handler service %s not found", service)
146+

0 commit comments

Comments
 (0)