Skip to content

Evok 3.1.0 - #276

Merged
matoushybl merged 277 commits into
mainfrom
release-3.1.0
Nov 2, 2026
Merged

matoushybl merged 277 commits into
mainfrom
release-3.1.0

Conversation

@miroslav-ondra

@miroslav-ondra miroslav-ondra commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Evok 3.1.0: dev-3.1 into main (271 commits, main has no commit missing in dev-3.1), with the release commits on top of dev-3.1.

The release commits

  • pyproject.toml and uv.lock: version 3.1.0, before 3.1.0a1.
  • The Czech copy of the API changes docs/zmeny-api-dev-src.md removed.

The links of the docs to the tag 3.1.0 (EVOK_VER, the udev rule of 1-Wire, examples/config.yaml) and the API changes against Evok 3.0.6 came from dev-3.1 (#277), the branch is merged into the release branch. The links work after the tag 3.1.0 is created.

Evok 3.1

The changes against 3.0 which affect the clients of the APIs, the configuration and the HW definitions, breaking changes first: docs/api-changes-dev-src.md; the major changes in README.md (#260).

The PRs merged into dev-3.1:

Testing

pytest tests: 1302 passed; uv lock --check passes; the wheel builds as evok-3.1.0-py3-none-any.whl.

Not changed

  • docs/spec.md reads Evok_API_OAS.yaml of the latest release, the workflow openapi attaches it to the release 3.1.0.

🤖 Generated with Claude Code

miroslav-ondra and others added 30 commits September 30, 2026 19:14
- new modbus/base.py with IODevice: alias, circuit, client and
  major_group, get(), set_alias() and _with_alias(); all modbus devices
  inherit from it and set devtype as a class attribute
- remove the unused legacy_mode from IOParser and the devices
- remove simple() (never called) from the modbus and 1-Wire devices
- remove get_state()/set_state() and ULED.value_delta(); the RPC
  handler uses get()/set() and keeps its response format
- NvSave: write the coil only for value=1, wait 200 ms, report
  value=1 only in the set() response; full() has no side effect
- Watchdog: read registers directly, simplify the timeout limit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- add .flake8 with max-line-length = 120
- whitespace, indentation, blank lines, redundant backslashes and
  one-line ifs fixed with autopep8
- wrap lines longer than 120 characters
- use 'except Exception' instead of bare except
- evok.py: move all imports above the logging setup
- owdevice.py: use 'is not None' and 'not in'

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- new modbus/iomode.py: IOMode keeps the mode register, the mode
  definitions, the current mode and its unit and range; update() reads
  the mode from the cache, set() validates the mode and writes it
- WithIOMode mixin exposes mode, modes, mode_value, regmode, unit_name
  and range on the device, so full() and the API are unchanged
- AnalogInput, AnalogOutput and AnalogOutputBrain use IOMode instead of
  three copies of reload_mode()/_apply_mode_data()
- AnalogInput builds the transformation only when the mode changes
- set() with an unknown mode now raises ValueError on all analog
  outputs, it was silently ignored on AO and Brain AO
- Client.read_i16(); DataPoint signed16 is sign-extended (xfail removed)
- tests for IOMode

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Reader reads a value from the Client cache and applies
  val * ratio + offset and rounding to decimals; subclasses implement
  only read_raw() for uint16, int16, uint32, int32 and float32
- 32-bit readers share Reader32 with word_order ("little"/"big")
- ReaderFactory.get() selects the class by datatype ('signed16' is an
  alias of int16 used by data_point), returns a Reader reading None for
  an unknown datatype, rejects word_order for 16-bit datatypes
- Reader.refactor() creates a reader of the same register, only index
  and is_input are kept, other parameters come from the call
- tests for readers, factory, transformations and refactor()

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- AnalogInput reads the value through a Reader; the mode transformation
  is built by Reader.refactor(), a mode without transformation reads
  float32 with 3 decimals
- AnalogOutput and AnalogOutputBrain derive from AnalogInput, the
  default reader is selected by _default_reader(), full() takes 'dev'
  from devtype
- AnalogOutput reads uint16 * 0.0025, the reader does not depend on mode
- AnalogOutputBrain reads the value or the resistance by the mode,
  set_value() checks the range of the current mode, the value cannot be
  set in Resistance or unknown mode, a mode change to Voltage/Current
  sets the value to 0
- DataPoint reads through a Reader, valid mask moved to new
  OwTemperature subclass used by the builder when valid_mask_reg is
  set; unused post_write removed
- tests updated

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Register reads uint16 through the DataPoint reader, set() writes
  the register, full() returns the value from the last scan
- registers are eventable devices, a change of the value emits an event
- value property and regvalue() removed, unused post parameter removed
- tests updated

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- new DIMode in iomode.py keeps the DirectSwitch mode and ds_mode of
  a digital input selected by its bit in the shared mode, polarity and
  toggle registers; update() reads the mode from the cache and reports
  a change, set() read-modify-writes the bits; WithDIMode mixin exposes
  the mode attributes
- DigitalInput uses DIMode, a mode change emits an event; reads value,
  counter and debounce through Readers, a disabled counter reads 0
- new ReaderBit reads a bit selected by mask from a 16-bit register;
  DigitalOutput, Relay, ULED and DigitalInput read the value through it
- new Client.write_u16() writes a register and updates the cache
- new pwm.py: PwmFrequency shared by the DO group with HardPwmFrequency
  (48 MHz timer, cycle * prescale) and SoftPwmFrequency (presets or
  1000 Hz / (1 + prescaler)); converts the duty between percent and
  the register value, a frequency out of range raises ValueError
- DigitalOutput takes pwm and pwmdutyreg, set_pwm_freq() keeps the duty
  of all outputs in the group; digital_only, block_pwm and copy() removed
- ULED derives from Relay, builder passes coil, reg, mask
- RPC input_get/input_get_value read from get(), DigitalInput.get()
  and get_value() removed
- tests updated, new tests for DIMode, ReaderBit and PWM frequency

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MySensor.set returned None, unlike the other devices. DataPoint.set
always raised; now it sets the alias and returns full(), and raises
ValueError only when a value is given, since a data point is read-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Prepares the classes for writing values, not only reading them.
Renames the Reader classes, ReaderFactory, the reader attributes
of devices and tests/test_reader.py. No functional change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Accessor.write applies the inverse transformation, rounds integer
datatypes and checks their range, rejects input registers and
writes through new Client.write_* methods, which update the cache.
AccessorBit does a read-modify-write of the masked bits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Register, AnalogOutput, AnalogOutputBrain, DigitalInput (counter,
debounce), DigitalOutput (PWM duty) and Watchdog write through
their accessors, so the cache is updated right after the write and
values out of range of the register are rejected.

Register.set rejects input registers instead of writing the holding
register at the same address. AnalogOutput.set_value rounds to the
nearest step instead of truncating and returns the value as full()
reports it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DataPoint.set writes the value through its accessor when the data
point has writable: true in the HW definition. The flag is false by
default, so the existing data points stay read-only. Register is
always writable and uses DataPoint.set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add POST /{format}/data_point/{circuit} with Data_point_params to the
API documentation and a JSON schema, which accepts only value and
alias. The value may be a string, as form posts send strings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Form posts send strings, so the register schema rejected every
/rest/register write. The range is still checked for numbers.
The di example used value, which the di schema does not allow.
All schema examples are now tested against their schemas.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A register keeps the bits of 16 outputs, but all outputs of a feature
read val_reg, so the outputs 17-32 reported the bits of 1-16. The
register is now val_reg + counter // 16, the coils stay contiguous.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IOMode reads and writes the mode register with AccessorU16, DIMode
uses AccessorBit for the mode, polarity and toggle bits and
OwTemperature reads its validity bit with AccessorBit. The regmode,
regpolarity and regtoggle device properties are removed.

IOMode.set() without a mode register writes nothing and accepts
any defined mode. OwTemperature reports valid=False before the
first scan instead of 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HardPwmFrequency and SoftPwmFrequency read the registers with
AccessorU16. _write() still writes through mb_client and updates
the cache after all writes, so update() never sees a partial setting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tail of the log file is read by GET /log?lines=N instead of
ModbusScanner.set(print_log=1), which returned bytes that could not
be serialized to JSON. The log is read in Python in a thread, the
unused evok_config parameter of ModbusScanner is removed.

NvSave keeps value=1 for 0.5 s after the write instead of sleeping
0.2 s in set() and reporting 1 only in its response. A new write is
refused while the timer runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
set() of all devices returns None, the REST and JSON API handlers
and the RPC methods return device.full() after it.

RPC: the owbus_set, owbus_scan and sensor_set methods await the
async set() instead of returning a coroutine, ao_set takes mode
instead of frequency, an invalid timeout raises ValueError and the
ai_set* methods for parameters AI does not have are removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Split the relay schema from the DO one, add schemas for nv_save and
the missing devtype altnames, drop fields set() does not accept and
allow alias for owpower. Convert watchdog reset/nv_save and owbus
intervals to numbers, reset the owbus only on a true do_reset.
Test that the schema fields equal the set() parameters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n REST

GET of a property reads it from full() instead of getattr(). by_name()
raises DeviceNotFound for an unknown devtype, circuit or alias. POST
rejects devices without a schema and returns only the first line of a
schema validation error. Unexpected exceptions are logged with the
traceback and reported as an internal error. Fix the list of circuit
'all' and build _get_all() with itertools.chain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Check the structure of a bulk request with a schema and the assigned
values of group_assignments by the device schema before setting any
device of the group. Look up the device before its schema, so an
unknown device is not found and a device without a schema is a bad
request. Report unexpected exceptions as an internal error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JSONBulkHandler goes to bulk_handler.py, WsHandler with registered_ws
to ws_handler.py. WsHandler gets all_filtered from the route instead of
reading the global evok_config. The websocket 'full' command sends the
state only to the requesting client without calling full() as a
coroutine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Describe the data_point and register values as they are returned
(NaN, null before the first read, alias) and accepted (numbers and
strings, no other fields). Add shared BadRequest and NotFound responses
to the device endpoints and descriptions to the data_point schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A duplicate or invalid alias raises ValueError, so the API reports
it as a bad request instead of an internal error. The whole alias must
match the allowed characters and an alias cannot be a circuit of the
same device type, by_name() would never find it. Clear the aliases
between tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
by_int() returned the device of an alias regardless of its type,
now it is not found as in by_name(). The alias dicts of Aliases were
class attributes shared by all instances.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An alias loaded from the file belongs to its device even when the
device is not registered yet (e.g. offline), it cannot be taken by
another device and lost on the next save. Such alias can be deleted
by POST /rest/run/alias with delete=<alias>. Loading of the alias file
does not fail on an unknown or missing devtype, the record is kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
full() of the devices tests the alias against '', a None alias was
returned as "alias": null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
by_int() and by_name() share the lookup of the device type, an unknown
type raises DeviceNotFound in both. by_int() filters by major_group in
one list. set_alias() leaves the logging of an invalid alias to the
caller. Remove remove_item(), BOARD and the unused unit constants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both looked up a device by the name of its type, by_int() only added
the major_group filter. by_name() takes major_group now, unknown
circuits are reported with the messages of by_name().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
miroslav-ondra and others added 6 commits October 26, 2026 18:54
… port 50200

- --config-dir (-d): the directory of autogen.yaml and of hw_definitions (the
  definitions of the Iris cards), /etc/evok by default, e.g. ~/evok/etc of Evok
  in a virtual environment
- --product-id (-p): the product id in hex (0407 or 0x0407) instead of
  UNIPI_PRODUCT_ID; the family is the low byte of the id, the last two digits
  of a 4-digit id before
- the default port of LOCAL_TCP is 50200, the Modbus TCP server of the Unipi OS,
  not 502; LISTEN_PORT of /etc/default/unipitcp goes first as before
- Docs: the options, generating autogen.yaml of a virtual environment, the port
  50200 of LOCAL_TCP in the examples and of the local I/Os, serial_ports in the
  autogen example, the API changes (en, cs)
- tests: the arguments, the environment, the port, no product id

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…arguments

- the default port of LOCAL_TCP is 502 again, 50200 on Unipi 1; the port of the
  Modbus TCP server of the Unipi OS is LISTEN_PORT of /etc/default/unipitcp
- an invalid --product-id or a missing --config-dir is an error of the
  arguments with the status 2, it printed "Device not recognized" with the
  status 0, the missing directory a traceback; nothing is written
- Docs: the variables of the environment, also for a run by hand (OWBUS only
  with HAS_DS2482=1), in the example of a virtual environment too; the rules of
  the port; the local I/Os on the port LISTEN_PORT
- tests: the ports, the invalid arguments

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- pyproject.toml and uv.lock: 3.1.0, before 3.1.0a1
- docs: the links to the udev rule of 1-Wire and to examples/config.yaml on
  main, EVOK_VER=main of the virtual environment, not dev-3.1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- docs/zmeny-api-dev-src.md removed, also from not_in_nav of mkdocs.yml
- api-changes-dev-src.md compiled between Evok 3.0 (b2e241c) and Evok 3.1,
  not between the branches main and dev-3.1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rors

- start_pulse() cancels the running sequence of the output after the
  successful write of the new value, under the lock of the unit
  (set_devices(replace_sequences=True)): a failed write, e.g. an unavailable
  unit, left the output in the state of the old pulse without its end
- get_api() of an empty section (websocket: with the options commented out)
  is {}, Evok did not start
- MQTT: an error of the connection which is not an MqttError (e.g. a missing
  file of TLS) is logged once and the handler connects again, the loop ended
  silently; an MqttError of a publish publishes the state again after the
  delay, the publishing task ended silently and the states were not published
- /rest: an empty body with Content-Type application/json sets nothing, it was
  a 400
- docs: the API changes against Evok 3.0.6 (main after it had the docs and
  #191); EVOK_VER=3.1.0 and the links to the tag 3.1.0; the replacement of
  a pulse in sequencer.md
- CI: the integration tests on the pushes to dev-*, not dev-3.1
- tests: the failed pulse, the old end before the new pulse, the empty section,
  the MQTT errors, the empty JSON body

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
miroslav-ondra and others added 10 commits October 26, 2026 20:24
…rrors, the API sections

- set_devices(sequences={device: Sequence}) starts the new sequence after the
  successful set() under the lock of the unit, it replaces the running one;
  start_pulse() started the pulse after the read of the unit, a task cancelled
  meanwhile left the output on without a sequence
- MQTT: a state refused by the client is published again after the other
  pending ones, at most 3 times with 1 s between them, then dropped; it was
  retried every 5 s forever and delayed the others. An error other than of
  MQTT is logged once also when it comes after each connection, the message
  says so
- the sections rpc, websocket, webhook and mqtt of apis are normalized when the
  config is read: an empty one is {}, another value than a mapping is an error
  of the config, Evok exits with it (also apis which is not a mapping); the
  callers do not repeat `or {}`
- tests: the pulse cancelled after its write, the dropped publish, the error
  after the connection, the API sections which are not a mapping

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The links of the docs and EVOK_VER on the tag 3.1.0 and the API changes against
Evok 3.0.6 of dev-3.1 replace the links to main and the wording of the release
commits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cks again

A unit may set a written register with a delay: xS52 sets the mode of its AI
in 0.3..0.8 s (checked by Modbus directly, xS51 at once). The read after the
write returned the old mode and overwrote the written value in the cache, the
reply and the event showed the old mode, the switch seemed to fail and the new
mode was seen by the next scan of the slow block, several seconds later; a
second switch showed the mode of the first one.

- Client.change() keeps the values written by write_registers() in the cache
  after the read, the reply and the event have them
- ModbusCacheMap.read_soon(): the blocks of the written registers are read by
  the periodic scan after REREAD_DELAY (1 s), also out of their scan_divider,
  their phase is kept; a value refused by the unit is seen then
- docs: the written registers after a change in the API changes, the check of
  the firmware before the release is done
- tests: the kept value, the block read again with the set and the refused value

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review of #278: the written value was kept only against the read of the
change. A periodic scan of the block before the unit set it (a block read by
every scan, or a slow one on its turn) read the old value and sent it, 2 -> 1
-> 2; the timer of an earlier write could read the block just after a next
write; a unit without the periodic scan never read a refused value again.

- ModbusCacheMap.pin(): a written holding register keeps its value for
  REREAD_DELAY, every read of its block keeps it (also get_register_async(), a
  read-modify-write of a bit uses the pinned value); a next write renews it.
  A group with an expired pin is read by the next periodic scan, out of its
  scan_divider and without a change of its counter, then the read value is used
- Client.write_registers() pins the values of a unit with the periodic scan,
  a unit without it shows the value read after the change
- read_soon(), the timers and Client._scan(written) removed
- docs: the pinned registers in the API changes; the check of the outputs
  written by a coil before the release
- tests: the block read after the pin, the scan during the pin, the renewed
  pin, a unit without the periodic scan, a bit written to the pinned value

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… long change

- write_coil_without_response() (reset, NV save) drops the pins of the unit:
  the read after the restart showed the written values instead of the saved
  settings
- a write without a pin (Accessor pin=False, e.g. the counter of a DI the unit
  counts on, or a unit without the periodic scan) drops the older pins of its
  registers, they overwrote the written value; the counter is not pinned, the
  read shows the count, it was 0 for 1 s
- RegisterGroup.pin() does not set the cache, the write or its caller does:
  PWM with cache=False updates it after all its writes, a check saw a partial
  setting
- the pins of a change are renewed at its end, a change longer than
  REREAD_DELAY lost the pins of its first writes before its read
- get_register_async() returns the cached values of the blocks, pin_expired()
  skips a group without pins
- tests: the restart, the counter, the unpinned write, cache=False, a long
  change

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the release 3.1.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ias before the write

- sequencer: a failed step is written again every STEP_RETRY_DELAY (1 s) until
  it succeeds or the sequence is cancelled, it is logged once; the last step
  ends the sequence after its successful write. A failed end of a pulse (e.g.
  a noise on the bus) ended the sequence before the write, the relay stayed on
  and finish_all() did not write it either. finish_all() does not write the
  final state of a sequence writing its last step again
- Watchdog: a changed timeout is an event; the timeout is checked before any
  change, a whole number of ms (it was cut by int()), an invalid one enabled
  the watchdog and set the alias before the error
- set_devices() checks the aliases of the assignments before the first write
  (Devices.check_alias()): the output was switched and then the request failed
  on a used, invalid or reserved alias
- AccessorBit writes with its pin
- docs: the retried step, the watchdog timeout, the alias in the API changes
  and sequencer.md
- tests: the retried step and the end after the write, the failed end of a DO
  pulse written again, the watchdog timeout, the alias, the bit without a pin

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
miroslav-ondra and others added 6 commits October 26, 2026 22:55
… errors, the aliases in order

- finish_all() writes the final state of a sequence whose last step was being
  written and failed (Running.finished after the successful write), it left
  the relay on
- only the errors of the communication are written again
  (UnitCommunicationError, TModbusError, TimeoutError, OSError), another
  error ends the sequence and is logged; a value refused by the output was
  written again forever and the output refused every change
- the step is done after its successful write, a step written again was
  reported as done
- set_devices() checks the aliases in the order of the changes (by the units),
  with the aliases released and taken by the earlier assignments: the same
  alias twice in one request passed the check, an alias moved to another
  device was refused
- set_alias() checks the alias by check_alias(), the rules in one place
- docs, tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ams, a non-string alias

- a step refused by the unit (an exception response Illegal Function, Data
  Address or Data Value, an invalid request) is not written again, it was an
  error of the communication (TModbusError) and was retried forever
- a step which ends its sequence by an error writes the final state of the
  sequence, the output stayed in its state
- the delay of a failed step doubles from 1 s up to 30 s, a missing unit does
  not load its bus every second; the failure is logged again when the error
  (with its cause) changes and every 5 minutes
- Watchdog.set() checks value and reset before any change too, an invalid one
  set the alias, enabled the watchdog or wrote the timeout before the error
- an alias which is not a string (0, false) is refused as invalid before the
  write, it passed as a reset and failed by TypeError after the write
- docs, tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on responses, the alias type

- the final state after a refused step is written as the last step of the
  sequence while it runs: it ended before, a new pulse or a change in the gap
  was overwritten by the old final state and finish_all() did not wait for it;
  an error of the communication writes it again, a noise lost it
- every exception response of the unit is a refused request (e.g. Server Device
  Failure was retried forever) except Server Device Busy, Acknowledge and the
  errors of a gateway, which are written again
- a failed step is logged again when the kind of its error changes, the text
  of a timeout differs by each attempt and was logged each time
- _check_aliases() checks the alias by check_alias() before the taken ones, a
  list alias raised TypeError (500)
- docs: the final state only of a sequence which has one, the watchdog timeout
  is cut to 65535
- tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… without the coil

- _check_aliases() skips an alias null as not given: it was checked as
  a reset and released the alias for another device, set_alias() keeps it and
  the duplicate failed after the write
- the final state after a refused step is not written when it is the refused
  step itself, e.g. the end of a pulse was sent again and logged twice
- Watchdog.set() ignores reset of a unit without the reset coil as before,
  an invalid one was refused
- docs: the PRs of the new entries of the API changes
- tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@matoushybl matoushybl added this to the v3.1 milestone Nov 2, 2026
@matoushybl
matoushybl merged commit 439f416 into main Nov 2, 2026
5 checks passed
@matoushybl
matoushybl deleted the release-3.1.0 branch November 2, 2026 14:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants