ha-dockermon icon indicating copy to clipboard operation
ha-dockermon copied to clipboard

Can't start container via rest template since HA 2023.6

Open jschaeke opened this issue 1 year ago • 26 comments

More info on https://community.home-assistant.io/t/rest-switch-stopped-working/581188

jschaeke avatar Jun 16 '23 15:06 jschaeke

Same problem

raulibi avatar Jun 16 '23 16:06 raulibi

I get the same error

evoteknic avatar Jun 16 '23 17:06 evoteknic

Thanks for reporting.

There might have been a change to the rest sensor in home assistant. I vaguely remember talking about it on a podcast episode recently, but could be mistaken.

I am using MQTT discovery via the dev branch. I'll see if I can replicate using a rest yaml switch to see what is up.

philhawthorne avatar Jun 17 '23 03:06 philhawthorne

Log:

Logger: homeassistant.components.rest.switch Source: components/rest/switch.py:180 Integration: RESTful (documentation, issues) First occurred: 10:37:07 AM (1 occurrences) Last logged: 10:37:07 AM

Error while switching off http://local.ip:8126/container/frigate

Not sure that's particularly helpful?

mitchellross avatar Jun 20 '23 01:06 mitchellross

Thanks @mitchellross

I can see the same for me at the moment.

The server I'm testing with doesn't seem to be working at all for me at the moment (even via Postman) so need to do a bit further digging.

I changed the command to use webhook.site. I suspect Home Assistant is posting the data raw, and not in the same way it used to (ie JSON or HTTP post fields)

The hunt continues for the moment

philhawthorne avatar Jun 20 '23 01:06 philhawthorne

Thanks Phil. Luckily its still reporting state fine to HA, so we can use a workaround for the moment with rest_commands and a script that turns off when state is on and vice versa.

Eg.

rest_command: temp_frigate_on: url: http://local.ip:8126/container/frigate/start temp_frigate_off: url: http://local.ip:8126/container/frigate/stop

and script:

alias: Temp frigate switch sequence:

  • choose:
    • conditions:
      • condition: state entity_id: switch.frigate state: "on" sequence:
      • service: rest_command.temp_frigate_off data: {}
    • conditions:
      • condition: state entity_id: switch.frigate state: "off" sequence:
      • service: rest_command.temp_frigate_on data: {} mode: restart

mitchellross avatar Jun 20 '23 04:06 mitchellross

Have nailed this down to Home Assistant no longer sending the content-type header application/octet-stream.

2023.5

curl -X 'POST' 'http://192.168.1.x:8126/container/xxx' -H 'connection: close' -H 'content-type: application/octet-stream' -H 'content-length: 18' -H 'accept-encoding: gzip, deflate' -H 'accept: */*' -H 'user-agent: HomeAssistant/2023.5.4 aiohttp/3.8.4 Python/3.10' -H 'host: 192.168.1.x' -d $'{"state": "start"}'

2023.6

curl -X 'POST' 'http://192.168.1.x:8126/container/xxx' -H 'connection: close' -H 'content-length: 18' -H 'user-agent: HomeAssistant/2023.6.1 httpx/0.24.1 Python/3.11' -H 'accept-encoding: gzip, deflate, br' -H 'accept: */*' -H 'host: 192.168.1.x' -H 'content-type: ' -d $'{"state": "start"}'

This could be a bug introduced when moving from aiohttp to httpx, which was done in this PR: https://github.com/home-assistant/core/pull/90768

~~Interestingly, as @jschaeke pointed out, adding the header back to the YAML doesn't seem to fix the switch, which sounds like this new library Home Assistant is using is removing that header.~~

I'll raise this as an issue in Home Assistant core and see what the Home Assistant devs think here. There might be other REST sensors/switches etc that are now broken/unsupported. If the devs decide on that side they won't put a change in, then I'll update HA-Dockermon to be compatible with 2023.6 moving forward.

philhawthorne avatar Jun 20 '23 10:06 philhawthorne

2023.6 Workaround

switch:
  - platform: rest
      resource: http://192.168.1.x:8126/container/xxx
      name: XXX Test
      body_on: '{"state": "start"}'
      body_off: '{"state": "stop"}'
      is_on_template: '{{ value_json is not none and value_json.state == "running" }}'
      headers:
        content-type: 'application/octet-stream'

Seems the content-type header is case sensitive, and in the case of HA-Dockermon expects it to be sent the same way 2023.5 would send it, lowercase.

philhawthorne avatar Jun 20 '23 10:06 philhawthorne

Workaround works indeed, I tried it indeed like that before, but probably, like you said, it had wrong capitalized casing.

jschaeke avatar Jun 20 '23 11:06 jschaeke

2023.6 Workaround

switch:
  - platform: rest
      resource: http://192.168.1.x:8126/container/xxx
      name: XXX Test
      body_on: '{"state": "start"}'
      body_off: '{"state": "stop"}'
      is_on_template: '{{ value_json is not none and value_json.state == "running" }}'
      headers:
        content-type: 'application/octet-stream'

Seems the content-type header is case sensitive, and in the case of HA-Dockermon expects it to be sent the same way 2023.5 would send it, lowercase.

Thanks much @philhawthorne, this works fine

hitesh-singh avatar Jun 25 '23 17:06 hitesh-singh

2023.6 Workaround

switch:
  - platform: rest
      resource: http://192.168.1.x:8126/container/xxx
      name: XXX Test
      body_on: '{"state": "start"}'
      body_off: '{"state": "stop"}'
      is_on_template: '{{ value_json is not none and value_json.state == "running" }}'
      headers:
        content-type: 'application/octet-stream'

Seems the content-type header is case sensitive, and in the case of HA-Dockermon expects it to be sent the same way 2023.5 would send it, lowercase.

My automations using ha-dockermon and rest switch work back again, thx @philhawthorne Since I was using these switches with my overnight automation only, I found issues about a week ago. Not know even on what version of HA it happened first time. I discovered it on 2023.6.3

TomAutoHome avatar Jul 24 '23 08:07 TomAutoHome

Same problem and also error in the HomeAssistant log:

ERROR (MainThread) [homeassistant.helpers.template] Template variable error: 'value_json' is undefined when rendering '{{ value_json is not none and value_json.state == "running" }}'

Trying the previous solution and it is not effective, I get an error when deactivating or activating switch:

ERROR (MainThread) [homeassistant.components.rest.switch] Error while switching off http://192.168.1.15:8126/container/tasmoadmin

rdlvm avatar Aug 23 '23 18:08 rdlvm

Is this issue resolved or not, I am not able to run the start/stop operation by following the workaround mentioned above. I get an error Error while switching off http://192.XXX.XXX.XXX:8126/container/frigate My configuration:

switch:
  - platform: rest
    resource: http://192.XXX.XXX.XXX:8126/container/frigate
    method: post
    name: Frigate
    username: redacted
    password: redacted
    body_on: '{"state": "start"}'
    body_off: '{"state": "stop"}'
    is_on_template: '{{ value_json is not none and value_json.state == "running" }}'
    headers:
      content-type: 'application/octet-stream'

I have tried with and without method:post The switch gets the state of the container fine but fails to either start or stop the same. I am on HA Core version 2023.10.3

vks007 avatar Oct 23 '23 16:10 vks007

Is this issue resolved or not, I am not able to run the start/stop operation by following the workaround mentioned above. I get an error Error while switching off http://192.XXX.XXX.XXX:8126/container/frigate My configuration:

switch:
  - platform: rest
    resource: http://192.XXX.XXX.XXX:8126/container/frigate
    method: post
    name: Frigate
    username: redacted
    password: redacted
    body_on: '{"state": "start"}'
    body_off: '{"state": "stop"}'
    is_on_template: '{{ value_json is not none and value_json.state == "running" }}'
    headers:
      content-type: 'application/octet-stream'

I have tried with and without method:post The switch gets the state of the container fine but fails to either start or stop the same. I am on HA Core version 2023.10.3

same problem.

rdlvm avatar Nov 17 '23 11:11 rdlvm

Just going to re-open this while the image hasn't been completely pushed. Seems as though Travis doesn't offer Open Source for free anymore, so need to migrate the builds to GitHub actions or CircleCI.

There is a new commit that should change ha-dockernon to respond to application/json calls. There is a change required to your YAML configuration for this to work:

headers:
  Content-Type: application/json

So a full switch might look like

switch:
    - platform: rest
      resource: http://127.0.0.1:8126/container/adguard
      name: Adguard
      body_on: '{"state": "start"}'
      body_off: '{"state": "stop"}'
      headers:
        Content-Type: application/json
      is_on_template: '{{ value_json is not none and value_json.state == "running" }}'

This is in line with the documentation for the Rest switch on the Home Assistant website.

I've manually pushed up a built image and tagged it as json. A docker run command is below. If as many people here can test and if it looks good, I'll roll it out to the latest tags.

docker run -d \
--name=ha-dockermon --restart=always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /path/to/config:/config \
-p 8126:8126 \
philhawthorne/ha-dockermon:json

philhawthorne avatar Nov 17 '23 12:11 philhawthorne

Well, weird, it works well since 2023.6 with application/octet-stream for me. I am currently on 2023.10.5. But maybe, I did not encounter this problem yet.

TomAutoHome avatar Nov 17 '23 12:11 TomAutoHome

Get request is working fine for me, but Post I keep getting 400 errors (UNRAID):

rawTrailers: [], joinDuplicateHeaders: undefined, aborted: false, upgrade: false, url: '/container/calibre', method: 'POST', statusCode: null, statusMessage: null, client: <ref *1> Socket { connecting: false, _hadError: false, _parent: null, _host: null, _closeAfterHandlingError: false, _readableState: ReadableState { objectMode: false, highWaterMark: 16384, buffer: BufferList { head: null, tail: null, length: 0 }, length: 0, pipes: [], flowing: true, ended: false, endEmitted: false, reading: true, constructed: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, errorEmitted: false, emitClose: false, autoDestroy: true, destroyed: false, errored: null, closed: false, closeEmitted: false, defaultEncoding: 'utf8', awaitDrainWriters: null, multiAwaitDrain: false, readingMore: false, dataEmitted: false, decoder: null, encoding: null, [Symbol(kPaused)]: false }, _events: [Object: null prototype] { end: [Array], timeout: [Function: socketOnTimeout], data: [Function: bound socketOnData], error: [Function: socketOnError], close: [Array], drain: [Function: bound socketOnDrain], resume: [Function: onSocketResume], pause: [Function: onSocketPause] }, _eventsCount: 8, _maxListeners: undefined, _writableState: WritableState { objectMode: false, highWaterMark: 16384, finalCalled: false, needDrain: false, ending: false, ended: false, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: true, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, afterWriteTickInfo: null, buffered: [], bufferedIndex: 0, allBuffers: true, allNoop: true, pendingcb: 0, constructed: true, prefinished: false, errorEmitted: false, emitClose: false, autoDestroy: true, errored: null, closed: false, closeEmitted: false, [Symbol(kOnFinished)]: [] }, allowHalfOpen: true, _sockname: null, _pendingData: null, _pendingEncoding: '', server: Server { maxHeaderSize: undefined, insecureHTTPParser: undefined, requestTimeout: 300000, headersTimeout: 60000, keepAliveTimeout: 5000, connectionsCheckingInterval: 30000, joinDuplicateHeaders: undefined, _events: [Object: null prototype], _eventsCount: 2, _maxListeners: undefined, _connections: 1, _handle: [TCP], _usingWorkers: false, _workers: [], _unref: false, allowHalfOpen: true, pauseOnConnect: false, noDelay: true, keepAlive: false, keepAliveInitialDelay: 0, httpAllowHalfOpen: false, timeout: 0, maxHeadersCount: null, maxRequestsPerSocket: 0, _connectionKey: '6::::8126', [Symbol(IncomingMessage)]: [Function: IncomingMessage], [Symbol(ServerResponse)]: [Function: ServerResponse], [Symbol(kCapture)]: false, [Symbol(async_id_symbol)]: 6, [Symbol(http.server.connections)]: ConnectionsList {}, [Symbol(http.server.connectionsCheckingInterval)]: Timeout { _idleTimeout: 30000, _idlePrev: [TimersList], _idleNext: [TimersList], _idleStart: 60168, _onTimeout: [Function: bound checkConnections], _timerArgs: undefined, _repeat: 30000, _destroyed: false, [Symbol(refed)]: false, [Symbol(kHasPrimitive)]: false, [Symbol(asyncId)]: 5, [Symbol(triggerId)]: 1 }, [Symbol(kUniqueHeaders)]: null }, _server: Server { maxHeaderSize: undefined, insecureHTTPParser: undefined, requestTimeout: 300000, headersTimeout: 60000, keepAliveTimeout: 5000, connectionsCheckingInterval: 30000, joinDuplicateHeaders: undefined, _events: [Object: null prototype], _eventsCount: 2, _maxListeners: undefined, _connections: 1, _handle: [TCP], _usingWorkers: false, _workers: [], _unref: false, allowHalfOpen: true, pauseOnConnect: false, noDelay: true, keepAlive: false, keepAliveInitialDelay: 0, httpAllowHalfOpen: false, timeout: 0, maxHeadersCount: null, maxRequestsPerSocket: 0, _connectionKey: '6::::8126', [Symbol(IncomingMessage)]: [Function: IncomingMessage], [Symbol(ServerResponse)]: [Function: ServerResponse], [Symbol(kCapture)]: false, [Symbol(async_id_symbol)]: 6, [Symbol(http.server.connections)]: ConnectionsList {}, [Symbol(http.server.connectionsCheckingInterval)]: Timeout { _idleTimeout: 30000, _idlePrev: [TimersList], _idleNext: [TimersList], _idleStart: 60168, _onTimeout: [Function: bound checkConnections], _timerArgs: undefined, _repeat: 30000, _destroyed: false, [Symbol(refed)]: false, [Symbol(kHasPrimitive)]: false, [Symbol(asyncId)]: 5, [Symbol(triggerId)]: 1 }, [Symbol(kUniqueHeaders)]: null }, parser: HTTPParser { '0': null, '1': [Function: parserOnHeaders], '2': [Function: parserOnHeadersComplete], '3': [Function: parserOnBody], '4': [Function: parserOnMessageComplete], '5': [Function: bound onParserExecute], '6': [Function: bound onParserTimeout], _headers: [], _url: '', socket: [Circular *1], incoming: [Circular *2], outgoing: null, maxHeaderPairs: 2000, _consumed: true, onIncoming: [Function: bound parserOnIncoming], joinDuplicateHeaders: undefined, [Symbol(resource_symbol)]: [HTTPServerAsyncResource] }, on: [Function: socketListenerWrap], addListener: [Function: socketListenerWrap], prependListener: [Function: socketListenerWrap], setEncoding: [Function: socketSetEncoding], _paused: false, _httpMessage: ServerResponse { _events: [Object: null prototype], _eventsCount: 1, _maxListeners: undefined, outputData: [], outputSize: 0, writable: true, destroyed: false, _last: false, chunkedEncoding: false, shouldKeepAlive: true, maxRequestsOnConnectionReached: false, _defaultKeepAlive: true, useChunkedEncodingByDefault: true, sendDate: true, _removedConnection: false, _removedContLen: false, _removedTE: false, strictContentLength: false, _contentLength: null, _hasBody: true, _trailer: '', finished: false, _headerSent: false, _closed: false, socket: [Circular *1], _header: null, _keepAliveTimeout: 5000, _onPendingData: [Function: bound updateOutgoingData], req: [Circular *2], _sent100: false, _expect_continue: false, _maxRequestsPerSocket: 0, locals: [Object: null prototype] {}, [Symbol(kCapture)]: false, [Symbol(kBytesWritten)]: 0, [Symbol(kNeedDrain)]: false, [Symbol(corked)]: 0, [Symbol(kOutHeaders)]: [Object: null prototype], [Symbol(errored)]: null, [Symbol(kUniqueHeaders)]: null }, [Symbol(async_id_symbol)]: 168, [Symbol(kHandle)]: TCP { reading: true, onconnection: null, _consumed: true, [Symbol(owner_symbol)]: [Circular *1] }, [Symbol(lastWriteQueueSize)]: 0, [Symbol(timeout)]: null, [Symbol(kBuffer)]: null, [Symbol(kBufferCb)]: null, [Symbol(kBufferGen)]: null, [Symbol(kCapture)]: false, [Symbol(kSetNoDelay)]: true, [Symbol(kSetKeepAlive)]: false, [Symbol(kSetKeepAliveInitialDelay)]: 0, [Symbol(kBytesRead)]: 0, [Symbol(kBytesWritten)]: 0 }, _consuming: false, _dumped: false, next: [Function: next], baseUrl: '', originalUrl: '/container/calibre', _parsedUrl: Url { protocol: null, slashes: null, auth: null, host: null, port: null, hostname: null, hash: null, search: null, query: null, pathname: '/container/calibre', path: '/container/calibre', href: '/container/calibre', _raw: '/container/calibre' }, params: { containerId: 'calibre' }, query: {}, res: <ref *3> ServerResponse { _events: [Object: null prototype] { finish: [Function: bound resOnFinish] }, _eventsCount: 1, _maxListeners: undefined, outputData: [], outputSize: 0, writable: true, destroyed: false, _last: false, chunkedEncoding: false, shouldKeepAlive: true, maxRequestsOnConnectionReached: false, _defaultKeepAlive: true, useChunkedEncodingByDefault: true, sendDate: true, _removedConnection: false, _removedContLen: false, _removedTE: false, strictContentLength: false, _contentLength: null, _hasBody: true, _trailer: '', finished: false, _headerSent: false, _closed: false, socket: <ref *1> Socket { connecting: false, _hadError: false, _parent: null, _host: null, _closeAfterHandlingError: false, _readableState: [ReadableState], _events: [Object: null prototype], _eventsCount: 8, _maxListeners: undefined, _writableState: [WritableState], allowHalfOpen: true, _sockname: null, _pendingData: null, _pendingEncoding: '', server: [Server], _server: [Server], parser: [HTTPParser], on: [Function: socketListenerWrap], addListener: [Function: socketListenerWrap], prependListener: [Function: socketListenerWrap], setEncoding: [Function: socketSetEncoding], _paused: false, _httpMessage: [Circular *3], [Symbol(async_id_symbol)]: 168, [Symbol(kHandle)]: [TCP], [Symbol(lastWriteQueueSize)]: 0, [Symbol(timeout)]: null, [Symbol(kBuffer)]: null, [Symbol(kBufferCb)]: null, [Symbol(kBufferGen)]: null, [Symbol(kCapture)]: false, [Symbol(kSetNoDelay)]: true, [Symbol(kSetKeepAlive)]: false, [Symbol(kSetKeepAliveInitialDelay)]: 0, [Symbol(kBytesRead)]: 0, [Symbol(kBytesWritten)]: 0 }, _header: null, _keepAliveTimeout: 5000, _onPendingData: [Function: bound updateOutgoingData], req: [Circular *2], _sent100: false, _expect_continue: false, _maxRequestsPerSocket: 0, locals: [Object: null prototype] {}, [Symbol(kCapture)]: false, [Symbol(kBytesWritten)]: 0, [Symbol(kNeedDrain)]: false, [Symbol(corked)]: 0, [Symbol(kOutHeaders)]: [Object: null prototype] { 'x-powered-by': [Array] }, [Symbol(errored)]: null, [Symbol(kUniqueHeaders)]: null }, body: {}, route: Route { path: '/container/:containerId', stack: [ [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer] ], methods: { acl: true, bind: true, checkout: true, connect: true, copy: true, delete: true, get: true, head: true, link: true, lock: true, 'm-search': true, merge: true, mkactivity: true, mkcalendar: true, mkcol: true, move: true, notify: true, options: true, patch: true, post: true, propfind: true, proppatch: true, purge: true, put: true, rebind: true, report: true, search: true, source: true, subscribe: true, trace: true, unbind: true, unlink: true, unlock: true, unsubscribe: true } }, [Symbol(kCapture)]: false, [Symbol(kHeaders)]: { 'content-type': 'application/json', 'postman-token': 'e9e500c3-f1c0-4207-8f44-5f5ce5ddcd44' }, [Symbol(kHeadersCount)]: 4, [Symbol(kTrailers)]: null, [Symbol(kTrailersCount)]: 0 } {}

Flight777 avatar Nov 17 '23 21:11 Flight777

Just going to re-open this while the image hasn't been completely pushed. Seems as though Travis doesn't offer Open Source for free anymore, so need to migrate the builds to GitHub actions or CircleCI.

There is a new commit that should change ha-dockernon to respond to application/json calls. There is a change required to your YAML configuration for this to work:

headers:
  Content-Type: application/json

So a full switch might look like

switch:
    - platform: rest
      resource: http://127.0.0.1:8126/container/adguard
      name: Adguard
      body_on: '{"state": "start"}'
      body_off: '{"state": "stop"}'
      headers:
        Content-Type: application/json
      is_on_template: '{{ value_json is not none and value_json.state == "running" }}'

This is in line with the documentation for the Rest switch on the Home Assistant website.

I've manually pushed up a built image and tagged it as json. A docker run command is below. If as many people here can test and if it looks good, I'll roll it out to the latest tags.

docker run -d \
--name=ha-dockermon --restart=always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /path/to/config:/config \
-p 8126:8126 \
philhawthorne/ha-dockermon:json

I couldn't try to install HA-Dockermon in json on my docker raspberry pi 4b, when starting I get an error: exec /usr/local/bin/docker-entrypoint.sh: exec format error

rdlvm avatar Nov 19 '23 11:11 rdlvm

Hmm unfortunately with Travis no longer having a free tier (and me not updating it) I haven't got ARM builds running yet :(

philhawthorne avatar Nov 19 '23 11:11 philhawthorne

Hmm unfortunately with Travis no longer having a free tier (and me not updating it) I haven't got ARM builds running yet :(

This means that there is no solution for raspberry pi 4 at the moment.

rdlvm avatar Nov 19 '23 11:11 rdlvm

Get request is working fine for me, but Post I keep getting 400 errors (UNRAID):

rawTrailers: [], joinDuplicateHeaders: undefined, aborted: false, upgrade: false, url: '/container/calibre', method: 'POST', statusCode: null, statusMessage: null, client: <ref *1> Socket { connecting: false, _hadError: false, _parent: null, _host: null, _closeAfterHandlingError: false, _readableState: ReadableState { objectMode: false, highWaterMark: 16384, buffer: BufferList { head: null, tail: null, length: 0 }, length: 0, pipes: [], flowing: true, ended: false, endEmitted: false, reading: true, constructed: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, errorEmitted: false, emitClose: false, autoDestroy: true, destroyed: false, errored: null, closed: false, closeEmitted: false, defaultEncoding: 'utf8', awaitDrainWriters: null, multiAwaitDrain: false, readingMore: false, dataEmitted: false, decoder: null, encoding: null, [Symbol(kPaused)]: false }, _events: [Object: null prototype] { end: [Array], timeout: [Function: socketOnTimeout], data: [Function: bound socketOnData], error: [Function: socketOnError], close: [Array], drain: [Function: bound socketOnDrain], resume: [Function: onSocketResume], pause: [Function: onSocketPause] }, _eventsCount: 8, _maxListeners: undefined, _writableState: WritableState { objectMode: false, highWaterMark: 16384, finalCalled: false, needDrain: false, ending: false, ended: false, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: true, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, afterWriteTickInfo: null, buffered: [], bufferedIndex: 0, allBuffers: true, allNoop: true, pendingcb: 0, constructed: true, prefinished: false, errorEmitted: false, emitClose: false, autoDestroy: true, errored: null, closed: false, closeEmitted: false, [Symbol(kOnFinished)]: [] }, allowHalfOpen: true, _sockname: null, _pendingData: null, _pendingEncoding: '', server: Server { maxHeaderSize: undefined, insecureHTTPParser: undefined, requestTimeout: 300000, headersTimeout: 60000, keepAliveTimeout: 5000, connectionsCheckingInterval: 30000, joinDuplicateHeaders: undefined, _events: [Object: null prototype], _eventsCount: 2, _maxListeners: undefined, _connections: 1, _handle: [TCP], _usingWorkers: false, _workers: [], _unref: false, allowHalfOpen: true, pauseOnConnect: false, noDelay: true, keepAlive: false, keepAliveInitialDelay: 0, httpAllowHalfOpen: false, timeout: 0, maxHeadersCount: null, maxRequestsPerSocket: 0, _connectionKey: '6::::8126', [Symbol(IncomingMessage)]: [Function: IncomingMessage], [Symbol(ServerResponse)]: [Function: ServerResponse], [Symbol(kCapture)]: false, [Symbol(async_id_symbol)]: 6, [Symbol(http.server.connections)]: ConnectionsList {}, [Symbol(http.server.connectionsCheckingInterval)]: Timeout { _idleTimeout: 30000, _idlePrev: [TimersList], _idleNext: [TimersList], _idleStart: 60168, _onTimeout: [Function: bound checkConnections], _timerArgs: undefined, _repeat: 30000, _destroyed: false, [Symbol(refed)]: false, [Symbol(kHasPrimitive)]: false, [Symbol(asyncId)]: 5, [Symbol(triggerId)]: 1 }, [Symbol(kUniqueHeaders)]: null }, _server: Server { maxHeaderSize: undefined, insecureHTTPParser: undefined, requestTimeout: 300000, headersTimeout: 60000, keepAliveTimeout: 5000, connectionsCheckingInterval: 30000, joinDuplicateHeaders: undefined, _events: [Object: null prototype], _eventsCount: 2, _maxListeners: undefined, _connections: 1, _handle: [TCP], _usingWorkers: false, _workers: [], _unref: false, allowHalfOpen: true, pauseOnConnect: false, noDelay: true, keepAlive: false, keepAliveInitialDelay: 0, httpAllowHalfOpen: false, timeout: 0, maxHeadersCount: null, maxRequestsPerSocket: 0, _connectionKey: '6::::8126', [Symbol(IncomingMessage)]: [Function: IncomingMessage], [Symbol(ServerResponse)]: [Function: ServerResponse], [Symbol(kCapture)]: false, [Symbol(async_id_symbol)]: 6, [Symbol(http.server.connections)]: ConnectionsList {}, [Symbol(http.server.connectionsCheckingInterval)]: Timeout { _idleTimeout: 30000, _idlePrev: [TimersList], _idleNext: [TimersList], _idleStart: 60168, _onTimeout: [Function: bound checkConnections], _timerArgs: undefined, _repeat: 30000, _destroyed: false, [Symbol(refed)]: false, [Symbol(kHasPrimitive)]: false, [Symbol(asyncId)]: 5, [Symbol(triggerId)]: 1 }, [Symbol(kUniqueHeaders)]: null }, parser: HTTPParser { '0': null, '1': [Function: parserOnHeaders], '2': [Function: parserOnHeadersComplete], '3': [Function: parserOnBody], '4': [Function: parserOnMessageComplete], '5': [Function: bound onParserExecute], '6': [Function: bound onParserTimeout], _headers: [], _url: '', socket: [Circular *1], incoming: [Circular *2], outgoing: null, maxHeaderPairs: 2000, _consumed: true, onIncoming: [Function: bound parserOnIncoming], joinDuplicateHeaders: undefined, [Symbol(resource_symbol)]: [HTTPServerAsyncResource] }, on: [Function: socketListenerWrap], addListener: [Function: socketListenerWrap], prependListener: [Function: socketListenerWrap], setEncoding: [Function: socketSetEncoding], _paused: false, _httpMessage: ServerResponse { _events: [Object: null prototype], _eventsCount: 1, _maxListeners: undefined, outputData: [], outputSize: 0, writable: true, destroyed: false, _last: false, chunkedEncoding: false, shouldKeepAlive: true, maxRequestsOnConnectionReached: false, _defaultKeepAlive: true, useChunkedEncodingByDefault: true, sendDate: true, _removedConnection: false, _removedContLen: false, _removedTE: false, strictContentLength: false, _contentLength: null, _hasBody: true, _trailer: '', finished: false, _headerSent: false, _closed: false, socket: [Circular *1], _header: null, _keepAliveTimeout: 5000, _onPendingData: [Function: bound updateOutgoingData], req: [Circular *2], _sent100: false, _expect_continue: false, _maxRequestsPerSocket: 0, locals: [Object: null prototype] {}, [Symbol(kCapture)]: false, [Symbol(kBytesWritten)]: 0, [Symbol(kNeedDrain)]: false, [Symbol(corked)]: 0, [Symbol(kOutHeaders)]: [Object: null prototype], [Symbol(errored)]: null, [Symbol(kUniqueHeaders)]: null }, [Symbol(async_id_symbol)]: 168, [Symbol(kHandle)]: TCP { reading: true, onconnection: null, _consumed: true, [Symbol(owner_symbol)]: [Circular *1] }, [Symbol(lastWriteQueueSize)]: 0, [Symbol(timeout)]: null, [Symbol(kBuffer)]: null, [Symbol(kBufferCb)]: null, [Symbol(kBufferGen)]: null, [Symbol(kCapture)]: false, [Symbol(kSetNoDelay)]: true, [Symbol(kSetKeepAlive)]: false, [Symbol(kSetKeepAliveInitialDelay)]: 0, [Symbol(kBytesRead)]: 0, [Symbol(kBytesWritten)]: 0 }, _consuming: false, _dumped: false, next: [Function: next], baseUrl: '', originalUrl: '/container/calibre', _parsedUrl: Url { protocol: null, slashes: null, auth: null, host: null, port: null, hostname: null, hash: null, search: null, query: null, pathname: '/container/calibre', path: '/container/calibre', href: '/container/calibre', _raw: '/container/calibre' }, params: { containerId: 'calibre' }, query: {}, res: <ref *3> ServerResponse { _events: [Object: null prototype] { finish: [Function: bound resOnFinish] }, _eventsCount: 1, _maxListeners: undefined, outputData: [], outputSize: 0, writable: true, destroyed: false, _last: false, chunkedEncoding: false, shouldKeepAlive: true, maxRequestsOnConnectionReached: false, _defaultKeepAlive: true, useChunkedEncodingByDefault: true, sendDate: true, _removedConnection: false, _removedContLen: false, _removedTE: false, strictContentLength: false, _contentLength: null, _hasBody: true, _trailer: '', finished: false, _headerSent: false, _closed: false, socket: <ref *1> Socket { connecting: false, _hadError: false, _parent: null, _host: null, _closeAfterHandlingError: false, _readableState: [ReadableState], _events: [Object: null prototype], _eventsCount: 8, _maxListeners: undefined, _writableState: [WritableState], allowHalfOpen: true, _sockname: null, _pendingData: null, _pendingEncoding: '', server: [Server], _server: [Server], parser: [HTTPParser], on: [Function: socketListenerWrap], addListener: [Function: socketListenerWrap], prependListener: [Function: socketListenerWrap], setEncoding: [Function: socketSetEncoding], _paused: false, _httpMessage: [Circular *3], [Symbol(async_id_symbol)]: 168, [Symbol(kHandle)]: [TCP], [Symbol(lastWriteQueueSize)]: 0, [Symbol(timeout)]: null, [Symbol(kBuffer)]: null, [Symbol(kBufferCb)]: null, [Symbol(kBufferGen)]: null, [Symbol(kCapture)]: false, [Symbol(kSetNoDelay)]: true, [Symbol(kSetKeepAlive)]: false, [Symbol(kSetKeepAliveInitialDelay)]: 0, [Symbol(kBytesRead)]: 0, [Symbol(kBytesWritten)]: 0 }, _header: null, _keepAliveTimeout: 5000, _onPendingData: [Function: bound updateOutgoingData], req: [Circular *2], _sent100: false, _expect_continue: false, _maxRequestsPerSocket: 0, locals: [Object: null prototype] {}, [Symbol(kCapture)]: false, [Symbol(kBytesWritten)]: 0, [Symbol(kNeedDrain)]: false, [Symbol(corked)]: 0, [Symbol(kOutHeaders)]: [Object: null prototype] { 'x-powered-by': [Array] }, [Symbol(errored)]: null, [Symbol(kUniqueHeaders)]: null }, body: {}, route: Route { path: '/container/:containerId', stack: [ [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer] ], methods: { acl: true, bind: true, checkout: true, connect: true, copy: true, delete: true, get: true, head: true, link: true, lock: true, 'm-search': true, merge: true, mkactivity: true, mkcalendar: true, mkcol: true, move: true, notify: true, options: true, patch: true, post: true, propfind: true, proppatch: true, purge: true, put: true, rebind: true, report: true, search: true, source: true, subscribe: true, trace: true, unbind: true, unlink: true, unlock: true, unsubscribe: true } }, [Symbol(kCapture)]: false, [Symbol(kHeaders)]: { 'content-type': 'application/json', 'postman-token': 'e9e500c3-f1c0-4207-8f44-5f5ce5ddcd44' }, [Symbol(kHeadersCount)]: 4, [Symbol(kTrailers)]: null, [Symbol(kTrailersCount)]: 0 } {}

@philhawthorne Do you have any pointers on this, is it a bug or something I'm missing? :)

Flight777 avatar Nov 20 '23 19:11 Flight777

Hmm unfortunately with Travis no longer having a free tier (and me not updating it) I haven't got ARM builds running yet :(

This means that there is no solution for raspberry pi 4 at the moment.

@rdlvm this should be available now, although I don't have a RPI4 to test with.

I've updated the GitHub actions, which has deployed this fix to the latest and arm-latest tags. So the fix should be available for Pis as well.

To confirm with docker-compose, you would need something like:

  ha-dockermon:
    image: philhawthorne/ha-dockermon:latest
    container_name: ha-dockermon
    restart: always
    ports:
      - 8126:8126
    volumes:
      - ./ha-dockermon:/config
      - /var/run/docker.sock:/var/run/docker.sock

Raspberry Pi:

  ha-dockermon:
    image: philhawthorne/ha-dockermon:arm-latest
    container_name: ha-dockermon
    restart: always
    ports:
      - 8126:8126
    volumes:
      - ./ha-dockermon:/config
      - /var/run/docker.sock:/var/run/docker.sock

Home Assistant Config

Your REST Switches need to be updated to:

  switch:
    - platform: rest
      resource: http://127.0.0.1:8126/container/adguard
      name: Adguard
      body_on: '{"state": "start"}'
      body_off: '{"state": "stop"}'
      headers:
        Content-Type: application/json
      is_on_template: '{{ value_json is not none and value_json.state == "running" }}'

philhawthorne avatar Nov 27 '23 13:11 philhawthorne

Get request is working fine for me, but Post I keep getting 400 errors (UNRAID):

rawTrailers: [], joinDuplicateHeaders: undefined, aborted: false, upgrade: false, url: '/container/calibre', method: 'POST', statusCode: null, statusMessage: null, client: <ref *1> Socket { connecting: false, _hadError: false, _parent: null, _host: null, _closeAfterHandlingError: false, _readableState: ReadableState { objectMode: false, highWaterMark: 16384, buffer: BufferList { head: null, tail: null, length: 0 }, length: 0, pipes: [], flowing: true, ended: false, endEmitted: false, reading: true, constructed: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, errorEmitted: false, emitClose: false, autoDestroy: true, destroyed: false, errored: null, closed: false, closeEmitted: false, defaultEncoding: 'utf8', awaitDrainWriters: null, multiAwaitDrain: false, readingMore: false, dataEmitted: false, decoder: null, encoding: null, [Symbol(kPaused)]: false }, _events: [Object: null prototype] { end: [Array], timeout: [Function: socketOnTimeout], data: [Function: bound socketOnData], error: [Function: socketOnError], close: [Array], drain: [Function: bound socketOnDrain], resume: [Function: onSocketResume], pause: [Function: onSocketPause] }, _eventsCount: 8, _maxListeners: undefined, _writableState: WritableState { objectMode: false, highWaterMark: 16384, finalCalled: false, needDrain: false, ending: false, ended: false, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: true, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, afterWriteTickInfo: null, buffered: [], bufferedIndex: 0, allBuffers: true, allNoop: true, pendingcb: 0, constructed: true, prefinished: false, errorEmitted: false, emitClose: false, autoDestroy: true, errored: null, closed: false, closeEmitted: false, [Symbol(kOnFinished)]: [] }, allowHalfOpen: true, _sockname: null, _pendingData: null, _pendingEncoding: '', server: Server { maxHeaderSize: undefined, insecureHTTPParser: undefined, requestTimeout: 300000, headersTimeout: 60000, keepAliveTimeout: 5000, connectionsCheckingInterval: 30000, joinDuplicateHeaders: undefined, _events: [Object: null prototype], _eventsCount: 2, _maxListeners: undefined, _connections: 1, _handle: [TCP], _usingWorkers: false, _workers: [], _unref: false, allowHalfOpen: true, pauseOnConnect: false, noDelay: true, keepAlive: false, keepAliveInitialDelay: 0, httpAllowHalfOpen: false, timeout: 0, maxHeadersCount: null, maxRequestsPerSocket: 0, _connectionKey: '6::::8126', [Symbol(IncomingMessage)]: [Function: IncomingMessage], [Symbol(ServerResponse)]: [Function: ServerResponse], [Symbol(kCapture)]: false, [Symbol(async_id_symbol)]: 6, [Symbol(http.server.connections)]: ConnectionsList {}, [Symbol(http.server.connectionsCheckingInterval)]: Timeout { _idleTimeout: 30000, _idlePrev: [TimersList], _idleNext: [TimersList], _idleStart: 60168, _onTimeout: [Function: bound checkConnections], _timerArgs: undefined, _repeat: 30000, _destroyed: false, [Symbol(refed)]: false, [Symbol(kHasPrimitive)]: false, [Symbol(asyncId)]: 5, [Symbol(triggerId)]: 1 }, [Symbol(kUniqueHeaders)]: null }, _server: Server { maxHeaderSize: undefined, insecureHTTPParser: undefined, requestTimeout: 300000, headersTimeout: 60000, keepAliveTimeout: 5000, connectionsCheckingInterval: 30000, joinDuplicateHeaders: undefined, _events: [Object: null prototype], _eventsCount: 2, _maxListeners: undefined, _connections: 1, _handle: [TCP], _usingWorkers: false, _workers: [], _unref: false, allowHalfOpen: true, pauseOnConnect: false, noDelay: true, keepAlive: false, keepAliveInitialDelay: 0, httpAllowHalfOpen: false, timeout: 0, maxHeadersCount: null, maxRequestsPerSocket: 0, _connectionKey: '6::::8126', [Symbol(IncomingMessage)]: [Function: IncomingMessage], [Symbol(ServerResponse)]: [Function: ServerResponse], [Symbol(kCapture)]: false, [Symbol(async_id_symbol)]: 6, [Symbol(http.server.connections)]: ConnectionsList {}, [Symbol(http.server.connectionsCheckingInterval)]: Timeout { _idleTimeout: 30000, _idlePrev: [TimersList], _idleNext: [TimersList], _idleStart: 60168, _onTimeout: [Function: bound checkConnections], _timerArgs: undefined, _repeat: 30000, _destroyed: false, [Symbol(refed)]: false, [Symbol(kHasPrimitive)]: false, [Symbol(asyncId)]: 5, [Symbol(triggerId)]: 1 }, [Symbol(kUniqueHeaders)]: null }, parser: HTTPParser { '0': null, '1': [Function: parserOnHeaders], '2': [Function: parserOnHeadersComplete], '3': [Function: parserOnBody], '4': [Function: parserOnMessageComplete], '5': [Function: bound onParserExecute], '6': [Function: bound onParserTimeout], _headers: [], _url: '', socket: [Circular *1], incoming: [Circular *2], outgoing: null, maxHeaderPairs: 2000, _consumed: true, onIncoming: [Function: bound parserOnIncoming], joinDuplicateHeaders: undefined, [Symbol(resource_symbol)]: [HTTPServerAsyncResource] }, on: [Function: socketListenerWrap], addListener: [Function: socketListenerWrap], prependListener: [Function: socketListenerWrap], setEncoding: [Function: socketSetEncoding], _paused: false, _httpMessage: ServerResponse { _events: [Object: null prototype], _eventsCount: 1, _maxListeners: undefined, outputData: [], outputSize: 0, writable: true, destroyed: false, _last: false, chunkedEncoding: false, shouldKeepAlive: true, maxRequestsOnConnectionReached: false, _defaultKeepAlive: true, useChunkedEncodingByDefault: true, sendDate: true, _removedConnection: false, _removedContLen: false, _removedTE: false, strictContentLength: false, _contentLength: null, _hasBody: true, _trailer: '', finished: false, _headerSent: false, _closed: false, socket: [Circular *1], _header: null, _keepAliveTimeout: 5000, _onPendingData: [Function: bound updateOutgoingData], req: [Circular *2], _sent100: false, _expect_continue: false, _maxRequestsPerSocket: 0, locals: [Object: null prototype] {}, [Symbol(kCapture)]: false, [Symbol(kBytesWritten)]: 0, [Symbol(kNeedDrain)]: false, [Symbol(corked)]: 0, [Symbol(kOutHeaders)]: [Object: null prototype], [Symbol(errored)]: null, [Symbol(kUniqueHeaders)]: null }, [Symbol(async_id_symbol)]: 168, [Symbol(kHandle)]: TCP { reading: true, onconnection: null, _consumed: true, [Symbol(owner_symbol)]: [Circular *1] }, [Symbol(lastWriteQueueSize)]: 0, [Symbol(timeout)]: null, [Symbol(kBuffer)]: null, [Symbol(kBufferCb)]: null, [Symbol(kBufferGen)]: null, [Symbol(kCapture)]: false, [Symbol(kSetNoDelay)]: true, [Symbol(kSetKeepAlive)]: false, [Symbol(kSetKeepAliveInitialDelay)]: 0, [Symbol(kBytesRead)]: 0, [Symbol(kBytesWritten)]: 0 }, _consuming: false, _dumped: false, next: [Function: next], baseUrl: '', originalUrl: '/container/calibre', _parsedUrl: Url { protocol: null, slashes: null, auth: null, host: null, port: null, hostname: null, hash: null, search: null, query: null, pathname: '/container/calibre', path: '/container/calibre', href: '/container/calibre', _raw: '/container/calibre' }, params: { containerId: 'calibre' }, query: {}, res: <ref *3> ServerResponse { _events: [Object: null prototype] { finish: [Function: bound resOnFinish] }, _eventsCount: 1, _maxListeners: undefined, outputData: [], outputSize: 0, writable: true, destroyed: false, _last: false, chunkedEncoding: false, shouldKeepAlive: true, maxRequestsOnConnectionReached: false, _defaultKeepAlive: true, useChunkedEncodingByDefault: true, sendDate: true, _removedConnection: false, _removedContLen: false, _removedTE: false, strictContentLength: false, _contentLength: null, _hasBody: true, _trailer: '', finished: false, _headerSent: false, _closed: false, socket: <ref *1> Socket { connecting: false, _hadError: false, _parent: null, _host: null, _closeAfterHandlingError: false, _readableState: [ReadableState], _events: [Object: null prototype], _eventsCount: 8, _maxListeners: undefined, _writableState: [WritableState], allowHalfOpen: true, _sockname: null, _pendingData: null, _pendingEncoding: '', server: [Server], _server: [Server], parser: [HTTPParser], on: [Function: socketListenerWrap], addListener: [Function: socketListenerWrap], prependListener: [Function: socketListenerWrap], setEncoding: [Function: socketSetEncoding], _paused: false, _httpMessage: [Circular *3], [Symbol(async_id_symbol)]: 168, [Symbol(kHandle)]: [TCP], [Symbol(lastWriteQueueSize)]: 0, [Symbol(timeout)]: null, [Symbol(kBuffer)]: null, [Symbol(kBufferCb)]: null, [Symbol(kBufferGen)]: null, [Symbol(kCapture)]: false, [Symbol(kSetNoDelay)]: true, [Symbol(kSetKeepAlive)]: false, [Symbol(kSetKeepAliveInitialDelay)]: 0, [Symbol(kBytesRead)]: 0, [Symbol(kBytesWritten)]: 0 }, _header: null, _keepAliveTimeout: 5000, _onPendingData: [Function: bound updateOutgoingData], req: [Circular *2], _sent100: false, _expect_continue: false, _maxRequestsPerSocket: 0, locals: [Object: null prototype] {}, [Symbol(kCapture)]: false, [Symbol(kBytesWritten)]: 0, [Symbol(kNeedDrain)]: false, [Symbol(corked)]: 0, [Symbol(kOutHeaders)]: [Object: null prototype] { 'x-powered-by': [Array] }, [Symbol(errored)]: null, [Symbol(kUniqueHeaders)]: null }, body: {}, route: Route { path: '/container/:containerId', stack: [ [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer], [Layer] ], methods: { acl: true, bind: true, checkout: true, connect: true, copy: true, delete: true, get: true, head: true, link: true, lock: true, 'm-search': true, merge: true, mkactivity: true, mkcalendar: true, mkcol: true, move: true, notify: true, options: true, patch: true, post: true, propfind: true, proppatch: true, purge: true, put: true, rebind: true, report: true, search: true, source: true, subscribe: true, trace: true, unbind: true, unlink: true, unlock: true, unsubscribe: true } }, [Symbol(kCapture)]: false, [Symbol(kHeaders)]: { 'content-type': 'application/json', 'postman-token': 'e9e500c3-f1c0-4207-8f44-5f5ce5ddcd44' }, [Symbol(kHeadersCount)]: 4, [Symbol(kTrailers)]: null, [Symbol(kTrailersCount)]: 0 } {}

Hmm not much here I can work with. I assume these are the errors spit out from the ha-dockermon container?

Can you try pulling the new latest tag. It has bumped the NodeJS version, and might solve it somehow.

philhawthorne avatar Nov 27 '23 13:11 philhawthorne

The above was spitout by the container :P

I've tried the latest tag just now, but still getting 400 bad request errors. Get is working perfectly.

Flight777 avatar Dec 02 '23 14:12 Flight777

Would be great if you could update, https://philhawthorne.com/ha-dockermon-use-home-assistant-to-monitor-start-or-stop-docker-containers/, too. Would have saved me ages :)

manut999 avatar Dec 03 '23 18:12 manut999

Hmm unfortunately with Travis no longer having a free tier (and me not updating it) I haven't got ARM builds running yet :(

This means that there is no solution for raspberry pi 4 at the moment.

@rdlvm this should be available now, although I don't have a RPI4 to test with.

I've updated the GitHub actions, which has deployed this fix to the latest and arm-latest tags. So the fix should be available for Pis as well.

To confirm with docker-compose, you would need something like:

  ha-dockermon:
    image: philhawthorne/ha-dockermon:latest
    container_name: ha-dockermon
    restart: always
    ports:
      - 8126:8126
    volumes:
      - ./ha-dockermon:/config
      - /var/run/docker.sock:/var/run/docker.sock

Raspberry Pi:

  ha-dockermon:
    image: philhawthorne/ha-dockermon:arm-latest
    container_name: ha-dockermon
    restart: always
    ports:
      - 8126:8126
    volumes:
      - ./ha-dockermon:/config
      - /var/run/docker.sock:/var/run/docker.sock

Home Assistant Config

Your REST Switches need to be updated to:

  switch:
    - platform: rest
      resource: http://127.0.0.1:8126/container/adguard
      name: Adguard
      body_on: '{"state": "start"}'
      body_off: '{"state": "stop"}'
      headers:
        Content-Type: application/json
      is_on_template: '{{ value_json is not none and value_json.state == "running" }}'

It seems to work now, I'll test it thoroughly and report my impressions.

thanks!!

rdlvm avatar Dec 05 '23 18:12 rdlvm