The “challenge” here isn’t first and foremost about programming, but about understanding the communication (and HTTP as a consequence). If the API was documented, we could have a fair shot of figuring this out. But, since it’s not, the only option is to try things and then meticulously record the result, so that you end up having a pretty good idea of how it will behave. Throw in that the API can change at any time on top of that, and you have a quite challenging situation to begin with. Doing this, “via forum”, seems all but impossible to me.
Thankfully, the original author has done most of this work already, but it’s quite essential to know which requests might reply with a JSON payload containing 401 and which ones might reply with a 401 status, since these situations must be treated very differently. Mapping this out for all the requests, using the forum, is just too much. It doesn’t feel realistic.
It’s easy to handle a 401 status on a generic basis, it’s not easy to do the same with a JSON payload. The reason for this is that the payload is just a string, and you must first do something with the string to make sense of it (usually deserializing it into a Java class). To do that, you must know what to expect, so that you can make logic that is prepared to deal with what might arrive.
I wouldn’t characterize what happens here as such a situation. Sure, if you get a broken connection once in a while, that’s completely normal, but you’re not supposed to get “HTTP protocol violation”. This can also be seen from how the binding code is written, it’s not “supposed to” or expecting this to happen. But, somehow it still ends up working. It should be handled differently, because the error you get is quite confusing, and just having to do “the research” to figure out why it happens isn’t something you should expect of people.
If I had one of these, it would be different of course, and I could use a camera or two. But, I’m hesitant, both because they are expensive and because it “feels” like I might be forced to use some “cloud solution”. I don’t use “cloud” solutions for any devices, I think it’s unacceptable privacy-wise, and in particular when it comes to security cameras. I would block all internet access for such cameras in the firewall, and they would still have to do what I need.
Instead of doing the research to try to figure out if that is possible, I’m choosing to just ask you guys that already know these: Is that possible/viable? No “cloud” account at all, no Internet connection. Will I still be able to use them using the local API, or will I be prevented from even doing that because I need some “cloud API key” or similar that I can only get by creating an online account?
I already have a UniFi AP, so I have a “UniFi Controller” service running on a server. I don’t know if this can also handle cameras, but if it can, it would suit me perfectly to use UniFi in that sense, as long as I can do it all locally.
I must confess I haven’t read everything in this thread but the discussion just rang a bell here: the UniFi network binding is suffering the same problem where connections are refused after two hours. A while ago I dug a while to find out that this was caused by an old jetty version in the dependency tree that does not support partitioned cookies. So this could be same problem here. There is also a lengthy thread about this on GitHub, however the problem was up to now that jetty is an upstream dependency of karaf and there were delays in the release cycle there. I have no idea if we are still in this situation, I have a workaround in place in my installation for now. I’ll search for the discussions and share the links here.
My understanding of that is next to zero, I’m afraid.
I did find this “nearly complete implementation of the UniFi Protect API”, but looking at the dates in the changelog (which seem to be after the start of the binding development), and searching the binding topic for some keywords, I don’t think this is what @Seaside used. (If that’s even possible, seeing how a binding needs to be Java, and this seems something else.)
I don’t think that’s enough. I’ve got a Unifi Dream Router, which can act as NVR from the box.
I’m afraid I’m gullible, and I go along with account stuff, and having the camera internet access. It’s also only directed at our yard, which is visible from the street anyway… I don’t think internet access is necessary, but an account might be required to set up the system…
That’s an interesting observation. I must admit that I don’t know what “partitioned cookies” is, but from some quick recon, it seems that OH 5 is using Jetty 9.4 and that “partitioned cookies” support seems to have been introduced in Jetty 10.
It’s a strange move of UniFi to go for something that was standardized so recently, it should at the very least be optional. Regardless, I’m pretty sure that’s not what’s going on here, the issue is pretty well understood (I think) and has to do with how token expiration is signalled and handled. The HTTP protocol requires that any 401 - Unauthorized response includes a header indicating what authentication schemes that are supported, so that the client can try again using a supported authentication scheme. UniFi doesn’t do this, which Jetty then throws a tantrum about. The reason UniFi doesn’t do this, is that it doesn’t use one of the “standardized authentication schemes” (as far as I can understand), so there’s probably no proper header to send. The argument by the people by Jetty is that in such a case, 401 shouldn’t be used, but some other code that doesn’t have the requirement for the header with valid authentication schemes to be sent with it.
But, as we have demonstrated above, we can turn off this behavior in Jetty, so that it doesn’t “interfere” with authentication and lets the binding handle it “manually”. But, something in the binding code doesn’t handle this correctly when Jetty doesn’t throw a tantrum, and exactly how to rectify that is hard to do without doing a lot of tests against a device to see how it responds.
In my nativity, I would think that it’s also possible to check whether the bootstrap payload is equal to {"error":{"code":401,"message":"Unauthorized"}}, and if so, have the binding to what it normally does when it encounters an expiry?
Of course an idea that doesn’t surpass the patchwork level…
You might be correct, I’m a bit thorough by nature I guess, but I don’t feel comfortable doing things unless I feel that I have an overview. Since the token can expire at any time, it follows that pretty much any request can be the one that “experiences the expiration”. So, I’m not very likely to try something if I feel that I only have a partial understanding, but it might be that “any error” makes the binding fetch a bootstrap info, and that all that would be needed is to handle it there. It might be worth a shot.
Ok, I did a little checking, and UniFi has definitely gone down a road that I find unacceptable. UniFi Protect can only run on their hardware and is tied to their cloud solution. They used to support self-hosting, but these products have been abandoned. I frankly wish nobody would buy such products, to signal to the manufacturers that this is an unacceptable direction to go. But, it seems, most people think it’s OK to have big companies have access to all kind of surveillance in their homes. I don’t understand it, but that’s just me, I guess. I, for one, won’t buy another UniFi product, even if they have some without privacy concerns, because I don’t want to condone/support such practices.
The UniFi network binding also just logs you back in when the connection has expired and retries the request. I don’t see why this should not be a good solution:
private <T> @Nullable T executeRequest(final UniFiControllerRequest<T> request, final boolean fromLogin)
throws UniFiException {
T result;
try {
result = (T) request.execute();
csrfToken = request.getCsrfToken();
} catch (final UniFiExpiredSessionException e) {
if (fromLogin) {
// if this exception is thrown from a login attempt something is wrong, because the login should init
// the session.
throw new UniFiCommunicationException(e);
} else {
login();
result = (T) executeRequest(request);
}
} catch (final UniFiNotAuthorizedException e) {
logger.warn("Not Authorized! Please make sure your controller credentials have administrator rights");
result = (T) null;
}
return result;
}
I can’t say for sure that it’s not related, it just doesn’t seem like this has to do with partitioned cookies, if that is the problem with the official UniFi binding. I see this in the official binding:
The HttpClient seems to be created here, without deactivating Jetty’s authentication handling:
So, the official binding has the same behavior - it expects to get 401 back when the session is expired, and it doesn’t make Jetty accept 401 without the required header. However, this isn’t a problem unless the device responds with a 401 without said header. Maybe this varies between different UniFi devices, or different firmware versions. If the official binding logs the same “HTTP protocol violation” exception, it’s pretty safe to say that the problem is also the same.
I’ve configured the official UniFi binding against my UniFi controller now, with debug logging enabled, so I assume I’ll see the exception at some point. I don’t usually use this binding as there’s nothing I need to do with the AP from OH. Enabling TRACE logging made it log the Wi-Fi password in clear text, although it’s hard to avoid since it logs the full requests and responses, and the password is revealed by the controller.
That’s a VERY long time to wait for debugging - chances are that one must repeat the procedure many times. I guess UniFi didn’t find it necessary to let us set the expiration value…
It’s been running for almost 5 hours now, and I see no such log entry, or anything else that indicates that it has reauthenticated. In fact, my log contains exactly one “login procedure”.
So, the question is if the rules for token expiration varies. I’m running the “UniFi Controller” software on a regular computer. I know that “UniFi Protect” is something different, but what devices or software are those of you that experience expiration for the official UniFi binding using? I’ve looked through the settings of “UniFi Controller”, and I can’t find any configuration related to this at all, so I doubt it is caused by different configurations.
Yeah, I configured my AP as a Thing and created a couple of Items. But, I don’t think that matters, I can see from the log that it keeps polling UniFi Controller constantly - every 10 seconds I think. As soon as the token expire, the refreshes would fail.
That’s one very expensive router - but I guess that if you consider it a “hardware firewall” the price becomes more acceptable, at least for businesses.
Regardless, it makes sense if this “security gateway” (in marketing lingo) expires the tokens more frequently than a free software that you can run to manage your devices. Maybe you can even adjust the expiration time on that one.
Another interesting note is that if my router (or more relevant: NVR) reboots, the channels aren’t updated until I manually disable the NVR thing and re-enable it again.
Disabling the thing renders this log:
2025-06-18 10:23:18.045 [DEBUG] [al.thing.UniFiProtectNvrThingHandler] - dispose()
2025-06-18 10:23:18.045 [DEBUG] [al.thing.UniFiProtectNvrThingHandler] - Cancelling refresh job
2025-06-18 10:23:18.045 [DEBUG] [ernal.event.UniFiProtectEventManager] - Failed to stop manager
java.lang.NullPointerException: Cannot invoke "org.eclipse.jetty.websocket.api.Session.disconnect()" because "this.session" is null
at org.openhab.binding.unifiprotect.websocket.UniFiProtectEventWebSocket.dispose(UniFiProtectEventWebSocket.java:153) ~[?:?]
at org.openhab.binding.unifiprotect.internal.event.UniFiProtectEventManager.stop(UniFiProtectEventManager.java:111) ~[?:?]
at org.openhab.binding.unifiprotect.internal.event.UniFiProtectEventManager.dispose(UniFiProtectEventManager.java:122) ~[?:?]
at org.openhab.binding.unifiprotect.internal.thing.UniFiProtectNvrThingHandler.dispose(UniFiProtectNvrThingHandler.java:194) ~[?:?]
at jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[?:?]
at java.lang.reflect.Method.invoke(Method.java:580) ~[?:?]
at org.openhab.core.internal.common.AbstractInvocationHandler.invokeDirect(AbstractInvocationHandler.java:147) ~[?:?]
at org.openhab.core.internal.common.Invocation.call(Invocation.java:52) ~[?:?]
at java.util.concurrent.FutureTask.run(FutureTask.java:317) ~[?:?]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) ~[?:?]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) ~[?:?]
at java.lang.Thread.run(Thread.java:1583) [?:?]
I’ve got the logs saved away in case someone ever wants to dive into this.