wirestudio 0.17.2__tar.gz → 0.18.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. {wirestudio-0.17.2/wirestudio.egg-info → wirestudio-0.18.0}/PKG-INFO +1 -1
  2. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_agent.py +13 -0
  3. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_api.py +16 -3
  4. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_compatibility.py +32 -0
  5. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_fleet.py +136 -2
  6. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_mcp_auth.py +42 -1
  7. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_mcp_server.py +15 -0
  8. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_modules.py +24 -0
  9. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_pin_solver.py +38 -0
  10. wirestudio-0.18.0/tests/test_strict.py +165 -0
  11. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/__init__.py +1 -1
  12. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/agent/tools.py +44 -2
  13. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/api/app.py +105 -30
  14. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/api/schemas.py +9 -1
  15. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/csp/__init__.py +11 -4
  16. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/csp/compatibility.py +69 -9
  17. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/csp/pin_solver.py +22 -5
  18. wirestudio-0.18.0/wirestudio/examples/solder-fan.json +41 -0
  19. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/fleet/client.py +44 -0
  20. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/generate/__main__.py +17 -0
  21. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/__init__.py +8 -0
  22. wirestudio-0.18.0/wirestudio/library/components/potentiometer.yaml +92 -0
  23. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/pulse_counter.yaml +1 -0
  24. wirestudio-0.18.0/wirestudio/library/components/pwm_fan.yaml +132 -0
  25. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/rtttl.yaml +1 -0
  26. wirestudio-0.18.0/wirestudio/library/modules/env-display.yaml +18 -0
  27. wirestudio-0.18.0/wirestudio/library/modules/fan-controller.yaml +22 -0
  28. wirestudio-0.18.0/wirestudio/library/modules/motion-light.yaml +17 -0
  29. wirestudio-0.18.0/wirestudio/library/modules/relay-4ch.yaml +22 -0
  30. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/mcp/__init__.py +6 -0
  31. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/mcp/auth.py +65 -14
  32. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/mcp/server.py +18 -0
  33. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/model.py +5 -0
  34. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/schema/design.schema.json +2 -0
  35. {wirestudio-0.17.2 → wirestudio-0.18.0/wirestudio.egg-info}/PKG-INFO +1 -1
  36. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio.egg-info/SOURCES.txt +8 -0
  37. {wirestudio-0.17.2 → wirestudio-0.18.0}/LICENSE +0 -0
  38. {wirestudio-0.17.2 → wirestudio-0.18.0}/README.md +0 -0
  39. {wirestudio-0.17.2 → wirestudio-0.18.0}/pyproject.toml +0 -0
  40. {wirestudio-0.17.2 → wirestudio-0.18.0}/setup.cfg +0 -0
  41. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_active_design.py +0 -0
  42. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_agent_stream.py +0 -0
  43. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_ascii_gen.py +0 -0
  44. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_build_backend.py +0 -0
  45. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_chirpstack.py +0 -0
  46. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_codec.py +0 -0
  47. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_compile.py +0 -0
  48. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_coverage_baseline.py +0 -0
  49. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_design_events.py +0 -0
  50. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_designs.py +0 -0
  51. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_designs_seed.py +0 -0
  52. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_enclosure.py +0 -0
  53. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_enclosure_gate.py +0 -0
  54. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_enclosure_search.py +0 -0
  55. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_firmware_gen.py +0 -0
  56. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_footprints.py +0 -0
  57. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_intent.py +0 -0
  58. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_inventory.py +0 -0
  59. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_jlcpcb.py +0 -0
  60. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_kicad.py +0 -0
  61. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_kicad_fab.py +0 -0
  62. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_kicad_import.py +0 -0
  63. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_kicad_pcb.py +0 -0
  64. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_kicad_render.py +0 -0
  65. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_library_rtttl.py +0 -0
  66. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_lorawan.py +0 -0
  67. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_lorawan_api.py +0 -0
  68. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_mcp_resources.py +0 -0
  69. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_recommend.py +0 -0
  70. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_schema.py +0 -0
  71. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_schematics_gate.py +0 -0
  72. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_security_cors.py +0 -0
  73. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_seed_onboard.py +0 -0
  74. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_serve.py +0 -0
  75. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_targets.py +0 -0
  76. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_tools_add_bus.py +0 -0
  77. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_tools_solve_pins.py +0 -0
  78. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_validate.py +0 -0
  79. {wirestudio-0.17.2 → wirestudio-0.18.0}/tests/test_yaml_gen.py +0 -0
  80. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/agent/__init__.py +0 -0
  81. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/agent/agent.py +0 -0
  82. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/agent/session.py +0 -0
  83. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/api/__init__.py +0 -0
  84. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/api/__main__.py +0 -0
  85. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/api/serve.py +0 -0
  86. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/designs/__init__.py +0 -0
  87. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/designs/active.py +0 -0
  88. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/designs/events.py +0 -0
  89. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/designs/seed.py +0 -0
  90. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/designs/store.py +0 -0
  91. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/enclosure/__init__.py +0 -0
  92. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/enclosure/openscad.py +0 -0
  93. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/enclosure/search.py +0 -0
  94. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/__init__.py +0 -0
  95. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/analog-node.json +0 -0
  96. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/atom-echo.json +0 -0
  97. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/atom-lite.json +0 -0
  98. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/atom-matrix.json +0 -0
  99. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/atoms3-lcd.json +0 -0
  100. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/atoms3-lite.json +0 -0
  101. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/atoms3-onboard.json +0 -0
  102. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/atomu-onboard.json +0 -0
  103. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/atomu.json +0 -0
  104. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/attic-logger.json +0 -0
  105. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/awning-control.json +0 -0
  106. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/bl0906-mainmeter.json +0 -0
  107. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/blind-stepper.json +0 -0
  108. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/bluemotion.json +0 -0
  109. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/bluesonoff.json +0 -0
  110. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/button-toggles-light.json +0 -0
  111. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/c3-supermini.json +0 -0
  112. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/c6-supermini.json +0 -0
  113. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/desk-climate.json +0 -0
  114. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/desk-matrix.json +0 -0
  115. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/distance-sensor.json +0 -0
  116. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/encoder-drives-stepper.json +0 -0
  117. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/esp32-audio.json +0 -0
  118. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/esp32cam-wrover.json +0 -0
  119. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/esp32cam.json +0 -0
  120. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/flow-meter.json +0 -0
  121. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/garage-motion.json +0 -0
  122. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/grill-probe.json +0 -0
  123. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/guarded-button-light.json +0 -0
  124. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/keypad.json +0 -0
  125. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/kitchen-scale.json +0 -0
  126. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/led-display.json +0 -0
  127. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/lorawan-battery-uplink.json +0 -0
  128. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/motion-turns-on-light.json +0 -0
  129. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/multi-temp.json +0 -0
  130. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/nextion-thermostat.json +0 -0
  131. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/oled-knob.json +0 -0
  132. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/oled.json +0 -0
  133. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/parking-distance.json +0 -0
  134. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/presence-rf.json +0 -0
  135. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/rc522.json +0 -0
  136. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/room-climate.json +0 -0
  137. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/rs485-energy.json +0 -0
  138. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/s3-devkitc.json +0 -0
  139. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/s3-supermini.json +0 -0
  140. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/securitypanel.json +0 -0
  141. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/smart-plug-v1.json +0 -0
  142. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/smart-plug.json +0 -0
  143. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/subghz-rfid.json +0 -0
  144. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/t-beam.json +0 -0
  145. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/temp-above-turns-on-fan.json +0 -0
  146. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/temp-turns-on-fan.json +0 -0
  147. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/tft-touch.json +0 -0
  148. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/ttgo-lora32.json +0 -0
  149. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/tuya-smart-plug.json +0 -0
  150. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/wasserpir.json +0 -0
  151. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/weather-station.json +0 -0
  152. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/examples/wemosgps.json +0 -0
  153. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/fleet/__init__.py +0 -0
  154. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/generate/__init__.py +0 -0
  155. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/generate/ascii_gen.py +0 -0
  156. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/generate/yaml_gen.py +0 -0
  157. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/intent.py +0 -0
  158. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/inventory/__init__.py +0 -0
  159. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/inventory/check.py +0 -0
  160. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/inventory/csv_io.py +0 -0
  161. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/inventory/store.py +0 -0
  162. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/jlcpcb/__init__.py +0 -0
  163. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/jlcpcb/__main__.py +0 -0
  164. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/jlcpcb/check.py +0 -0
  165. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/jlcpcb/client.py +0 -0
  166. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/kicad/__init__.py +0 -0
  167. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/kicad/fab.py +0 -0
  168. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/kicad/generator.py +0 -0
  169. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/kicad/import.py +0 -0
  170. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/kicad/importer.py +0 -0
  171. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/kicad/netlist.py +0 -0
  172. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/kicad/pcb.py +0 -0
  173. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/kicad/render.py +0 -0
  174. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/kicad/symbol_parser.py +0 -0
  175. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/boards/esp01_1m.yaml +0 -0
  176. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/boards/esp32-c3-devkitm-1.yaml +0 -0
  177. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/boards/esp32-c3-supermini.yaml +0 -0
  178. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/boards/esp32-c6-supermini.yaml +0 -0
  179. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/boards/esp32-devkitc-v4.yaml +0 -0
  180. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/boards/esp32-s3-devkitc-1.yaml +0 -0
  181. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/boards/esp32-s3-supermini.yaml +0 -0
  182. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/boards/esp32-wrover-cam.yaml +0 -0
  183. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/boards/esp32cam-ai-thinker.yaml +0 -0
  184. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/boards/esp8285-1m.yaml +0 -0
  185. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/boards/heltec-wifi-lora32-v2.yaml +0 -0
  186. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/boards/heltec-wifi-lora32-v3.yaml +0 -0
  187. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/boards/m5stack-atom-echo.yaml +0 -0
  188. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/boards/m5stack-atom-matrix.yaml +0 -0
  189. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/boards/m5stack-atom.yaml +0 -0
  190. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/boards/m5stack-atoms3-lite.yaml +0 -0
  191. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/boards/m5stack-atoms3.yaml +0 -0
  192. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/boards/m5stack-atomu.yaml +0 -0
  193. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/boards/nodemcu-32s.yaml +0 -0
  194. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/boards/nodemcu-v2.yaml +0 -0
  195. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/boards/ttgo-lora32-v1.yaml +0 -0
  196. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/boards/ttgo-lora32-v2.yaml +0 -0
  197. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/boards/ttgo-t-beam.yaml +0 -0
  198. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/boards/wemos-d1-mini.yaml +0 -0
  199. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/adc.yaml +0 -0
  200. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/ads1115.yaml +0 -0
  201. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/ads1115_channel.yaml +0 -0
  202. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/aht10.yaml +0 -0
  203. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/apa102.yaml +0 -0
  204. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/axp192.yaml +0 -0
  205. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/bh1750.yaml +0 -0
  206. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/bl0906.yaml +0 -0
  207. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/bme280.yaml +0 -0
  208. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/bmp180.yaml +0 -0
  209. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/bmp280.yaml +0 -0
  210. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/cc1101.yaml +0 -0
  211. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/cse7766.yaml +0 -0
  212. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/dht.yaml +0 -0
  213. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/ds18b20.yaml +0 -0
  214. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/esp32_camera.yaml +0 -0
  215. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/esp32_rmt_led_strip.yaml +0 -0
  216. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/gpio_input.yaml +0 -0
  217. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/gpio_output.yaml +0 -0
  218. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/hc-sr04.yaml +0 -0
  219. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/hc-sr501.yaml +0 -0
  220. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/hlw8012.yaml +0 -0
  221. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/htu21d.yaml +0 -0
  222. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/hx711.yaml +0 -0
  223. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/i2s_microphone.yaml +0 -0
  224. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/ili9xxx.yaml +0 -0
  225. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/lcd_pcf8574.yaml +0 -0
  226. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/ld2420.yaml +0 -0
  227. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/max31855.yaml +0 -0
  228. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/max7219.yaml +0 -0
  229. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/max98357a.yaml +0 -0
  230. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/mcp23008.yaml +0 -0
  231. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/mcp23017.yaml +0 -0
  232. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/modbus.yaml +0 -0
  233. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/mpu6050.yaml +0 -0
  234. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/mpu6886.yaml +0 -0
  235. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/nextion.yaml +0 -0
  236. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/pcf8574.yaml +0 -0
  237. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/rc522.yaml +0 -0
  238. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/rcwl-0516.yaml +0 -0
  239. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/rdm6300.yaml +0 -0
  240. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/remote_transmitter.yaml +0 -0
  241. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/rf_bridge.yaml +0 -0
  242. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/rotary_encoder.yaml +0 -0
  243. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/sdm_meter.yaml +0 -0
  244. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/sht3xd.yaml +0 -0
  245. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/ssd1306.yaml +0 -0
  246. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/st7789.yaml +0 -0
  247. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/sx127x.yaml +0 -0
  248. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/tm1638.yaml +0 -0
  249. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/tsl2561.yaml +0 -0
  250. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/tuya.yaml +0 -0
  251. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/tuya_sensor.yaml +0 -0
  252. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/tuya_switch.yaml +0 -0
  253. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/uart_gps.yaml +0 -0
  254. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/uln2003.yaml +0 -0
  255. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/vl53l0x.yaml +0 -0
  256. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/ws2812b.yaml +0 -0
  257. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/components/xpt2046.yaml +0 -0
  258. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/library/modules/oled-knob-13.yaml +0 -0
  259. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/recommend/__init__.py +0 -0
  260. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/recommend/recommender.py +0 -0
  261. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/schema/__init__.py +0 -0
  262. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/seed.py +0 -0
  263. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/targets/__init__.py +0 -0
  264. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/targets/base.py +0 -0
  265. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/targets/build_backend.py +0 -0
  266. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/targets/esphome.py +0 -0
  267. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/targets/lorawan/__init__.py +0 -0
  268. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/targets/lorawan/__main__.py +0 -0
  269. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/targets/lorawan/api.py +0 -0
  270. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/targets/lorawan/build_local.py +0 -0
  271. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/targets/lorawan/chirpstack.py +0 -0
  272. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/targets/lorawan/codec.py +0 -0
  273. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/targets/lorawan/compile.py +0 -0
  274. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/targets/lorawan/firmware_gen.py +0 -0
  275. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/targets/lorawan/templates/main.cpp.j2 +0 -0
  276. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/targets/lorawan/templates/platformio.ini.j2 +0 -0
  277. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio/validate.py +0 -0
  278. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio.egg-info/dependency_links.txt +0 -0
  279. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio.egg-info/entry_points.txt +0 -0
  280. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio.egg-info/requires.txt +0 -0
  281. {wirestudio-0.17.2 → wirestudio-0.18.0}/wirestudio.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: wirestudio
3
- Version: 0.17.2
3
+ Version: 0.18.0
4
4
  Summary: Agent-driven IoT device design tool that produces ESPHome YAML.
5
5
  Author: wirestudio contributors
6
6
  License: MIT
@@ -145,6 +145,19 @@ def test_set_param_set_and_delete(lib, garage_motion_design):
145
145
  assert "address" not in bme1["params"]
146
146
 
147
147
 
148
+ def test_set_strict_toggles_design_field(lib, garage_motion_design):
149
+ assert garage_motion_design.get("strict", False) is False
150
+ out, is_error = execute_tool(
151
+ "set_strict", {"enabled": True}, garage_motion_design, lib,
152
+ )
153
+ assert is_error is False
154
+ assert garage_motion_design["strict"] is True
155
+ assert json.loads(out)["strict"] is True
156
+
157
+ execute_tool("set_strict", {"enabled": False}, garage_motion_design, lib)
158
+ assert garage_motion_design["strict"] is False
159
+
160
+
148
161
  def test_set_connection_updates_existing(lib, garage_motion_design):
149
162
  out, is_error = execute_tool(
150
163
  "set_connection",
@@ -160,13 +160,26 @@ def test_render_strict_mode_blocks_on_compat_warning(client):
160
160
  assert strict.status_code == 422
161
161
  detail = strict.json()["detail"]
162
162
  assert detail["error"] == "strict_mode_blocked"
163
- assert "compatibility issue" in detail["message"]
164
- assert len(detail["warnings"]) >= 1
163
+ assert "need attention" in detail["message"]
164
+ assert len(detail["blockers"]) >= 1
165
165
  assert all(
166
- w["severity"] in ("warn", "error") for w in detail["warnings"]
166
+ b["severity"] in ("warn", "error") for b in detail["blockers"]
167
167
  )
168
168
 
169
169
 
170
+ def test_render_strict_via_design_field_blocks(client):
171
+ """`strict: true` on the design blocks generation with no ?strict query --
172
+ the toggle lives in design.json so generation stays a pure function of it."""
173
+ design = json.loads((EXAMPLES_DIR / "ttgo-lora32.json").read_text())
174
+ design["strict"] = True
175
+ blocked = client.post("/design/render", json=design)
176
+ assert blocked.status_code == 422
177
+ assert blocked.json()["detail"]["error"] == "strict_mode_blocked"
178
+
179
+ design["strict"] = False
180
+ assert client.post("/design/render", json=design).status_code == 200
181
+
182
+
170
183
  def test_render_strict_mode_ignores_info_severity(client):
171
184
  """info-severity entries (like the D1 Mini A0 voltage_limit) should
172
185
  NOT trip strict mode -- they're educational, not blocking. We use
@@ -139,6 +139,38 @@ def test_a0_voltage_limit_emits_info(lib):
139
139
  assert "input_only" in set(lib_d1.gpio_capabilities["A0"])
140
140
 
141
141
 
142
+ def test_pwm_output_on_no_pwm_pin_is_an_error(lib):
143
+ """D1 Mini's D0 (GPIO16) has no timer PWM. Hand-wiring pwm_fan.PWM (a
144
+ requires_pwm output) there should emit a hard error -- the solver won't
145
+ auto-assign it, but a manual bind must still be caught."""
146
+ d = _load("solder-fan")
147
+ for c in d["connections"]:
148
+ if c["component_id"] == "exhaust" and c["pin_role"] == "PWM":
149
+ c["target"] = {"kind": "gpio", "pin": "D0"}
150
+ warnings = check_pin_compatibility(d, lib)
151
+ errs = [w for w in _by_code(warnings, "function_unsupported")
152
+ if w.pin == "D0" and w.pin_role == "PWM"]
153
+ assert len(errs) == 1
154
+ assert errs[0].severity == "error"
155
+ assert "PWM" in errs[0].message
156
+
157
+
158
+ def test_interrupt_input_on_no_interrupt_pin_is_an_error(lib):
159
+ """D1 Mini's D0 (GPIO16) can't fire edge interrupts. Hand-wiring the fan's
160
+ tachometer (pulse_counter, a requires_interrupt input) there should emit a
161
+ hard error."""
162
+ d = _load("solder-fan")
163
+ for c in d["connections"]:
164
+ if c["component_id"] == "exhaust" and c["pin_role"] == "TACH":
165
+ c["target"] = {"kind": "gpio", "pin": "D0"}
166
+ warnings = check_pin_compatibility(d, lib)
167
+ errs = [w for w in _by_code(warnings, "function_unsupported")
168
+ if w.pin == "D0" and w.pin_role == "TACH"]
169
+ assert len(errs) == 1
170
+ assert errs[0].severity == "error"
171
+ assert "interrupt" in errs[0].message
172
+
173
+
142
174
  def test_no_i2c_pin_used_as_i2c_bus_warns(lib):
143
175
  """D1 Mini's D0 is tagged no_i2c. If the user routes an I2C bus's SDA or
144
176
  SCL through D0, the validator should warn."""
@@ -43,6 +43,11 @@ class FakeFleetAddon:
43
43
  # ({"id", "run_id", "state", "target", "finished_at"}) to drive the
44
44
  # run-status verdict tests.
45
45
  self.queue_jobs: list[dict] = []
46
+ # run_id -> {"app": bytes | None, "factory": bytes | None}; tests
47
+ # populate this to drive the firmware-artifact passthrough tests.
48
+ # None means "not built" -> the addon 404s (matches the fleet-side
49
+ # contract that 404 = not ready or unavailable).
50
+ self.firmware: dict[str, dict[str, bytes | None]] = {}
46
51
 
47
52
  def transport(self) -> httpx.MockTransport:
48
53
  def handler(req: httpx.Request) -> httpx.Response:
@@ -110,6 +115,26 @@ class FakeFleetAddon:
110
115
  "finished": bool(self.job_logs[run_id]["finished"]),
111
116
  })
112
117
 
118
+ # GET /ui/api/jobs/{run_id}/firmware -> app image
119
+ # GET /ui/api/jobs/{run_id}/firmware/factory -> factory image
120
+ if method == "GET" and path.startswith("/ui/api/jobs/") and (
121
+ path.endswith("/firmware") or path.endswith("/firmware/factory")
122
+ ):
123
+ kind = "factory" if path.endswith("/factory") else "app"
124
+ trim = "/firmware/factory" if kind == "factory" else "/firmware"
125
+ run_id = path[len("/ui/api/jobs/"):-len(trim)]
126
+ slot = self.firmware.get(run_id)
127
+ if slot is None:
128
+ return httpx.Response(404, json={"error": "unknown run_id"})
129
+ data = slot.get(kind)
130
+ if data is None:
131
+ return httpx.Response(404, json={"error": f"no {kind} image"})
132
+ return httpx.Response(
133
+ 200,
134
+ content=data,
135
+ headers={"content-type": "application/octet-stream"},
136
+ )
137
+
113
138
  return httpx.Response(404, json={"error": "not found"})
114
139
 
115
140
  return httpx.MockTransport(handler)
@@ -342,8 +367,8 @@ async def test_fleet_push_strict_blocks_on_compat_warning(monkeypatch, tmp_path)
342
367
  assert strict.status_code == 422
343
368
  detail = strict.json()["detail"]
344
369
  assert detail["error"] == "strict_mode_blocked"
345
- assert "compatibility issue" in detail["message"]
346
- assert all(w["severity"] in ("warn", "error") for w in detail["warnings"])
370
+ assert "need attention" in detail["message"]
371
+ assert all(b["severity"] in ("warn", "error") for b in detail["blockers"])
347
372
  # The push must NOT have hit the addon when strict refuses.
348
373
  assert len(addon.compile_runs) == 0
349
374
 
@@ -488,6 +513,115 @@ async def test_fleet_job_log_unknown_run_id_returns_502(monkeypatch, tmp_path):
488
513
  assert r.status_code == 502
489
514
 
490
515
 
516
+ # ---------------------------------------------------------------------------
517
+ # Build-artifact passthrough (fleet -> studio -> browser WebSerial)
518
+ # ---------------------------------------------------------------------------
519
+
520
+ async def test_get_firmware_returns_app_image_bytes():
521
+ addon = FakeFleetAddon()
522
+ addon.firmware["run-1"] = {"app": b"FAKE-APP-IMAGE", "factory": None}
523
+ fc = addon.make_client()
524
+ data = await fc.get_firmware("run-1")
525
+ assert data == b"FAKE-APP-IMAGE"
526
+
527
+
528
+ async def test_get_firmware_factory_uses_factory_subpath():
529
+ addon = FakeFleetAddon()
530
+ addon.firmware["run-1"] = {
531
+ "app": b"APP",
532
+ "factory": b"BOOT+PART+APP",
533
+ }
534
+ fc = addon.make_client()
535
+ data = await fc.get_firmware("run-1", factory=True)
536
+ assert data == b"BOOT+PART+APP"
537
+
538
+
539
+ async def test_get_firmware_unknown_run_id_raises():
540
+ addon = FakeFleetAddon()
541
+ fc = addon.make_client()
542
+ with pytest.raises(FleetUnavailable, match="not available"):
543
+ await fc.get_firmware("nope")
544
+
545
+
546
+ async def test_get_firmware_factory_missing_raises():
547
+ # The build produced an app image but the older ESPHome on the fleet
548
+ # didn't emit firmware-factory.bin -- the addon 404s factory while the
549
+ # app image is fine. The dialog falls back / surfaces the message.
550
+ addon = FakeFleetAddon()
551
+ addon.firmware["run-1"] = {"app": b"APP", "factory": None}
552
+ fc = addon.make_client()
553
+ with pytest.raises(FleetUnavailable, match="not available"):
554
+ await fc.get_firmware("run-1", factory=True)
555
+
556
+
557
+ async def test_get_firmware_unconfigured_raises():
558
+ fc = FleetClient(base_url=None, token=None)
559
+ with pytest.raises(FleetUnavailable, match="missing"):
560
+ await fc.get_firmware("run-1")
561
+
562
+
563
+ async def test_fleet_firmware_endpoint_unconfigured_returns_503(monkeypatch, tmp_path):
564
+ client = _make_client(monkeypatch, tmp_path, addon=None)
565
+ r = client.get("/fleet/jobs/run-1/firmware")
566
+ assert r.status_code == 503
567
+
568
+
569
+ async def test_fleet_firmware_endpoint_round_trips_app_image(monkeypatch, tmp_path):
570
+ addon = FakeFleetAddon()
571
+ addon.firmware["run-7"] = {"app": b"\x01\x02APP", "factory": None}
572
+ client = _make_client(monkeypatch, tmp_path, addon=addon)
573
+ r = client.get("/fleet/jobs/run-7/firmware")
574
+ assert r.status_code == 200
575
+ assert r.headers["content-type"] == "application/octet-stream"
576
+ assert r.headers["content-disposition"] == 'attachment; filename="run-7.bin"'
577
+ assert r.content == b"\x01\x02APP"
578
+
579
+
580
+ async def test_fleet_firmware_endpoint_round_trips_factory_image(monkeypatch, tmp_path):
581
+ addon = FakeFleetAddon()
582
+ addon.firmware["run-7"] = {"app": b"app", "factory": b"\xe9\x00\x00FACTORY"}
583
+ client = _make_client(monkeypatch, tmp_path, addon=addon)
584
+ r = client.get("/fleet/jobs/run-7/firmware?factory=true")
585
+ assert r.status_code == 200
586
+ assert r.headers["content-disposition"] == 'attachment; filename="run-7-factory.bin"'
587
+ assert r.content == b"\xe9\x00\x00FACTORY"
588
+
589
+
590
+ async def test_fleet_firmware_endpoint_unknown_run_id_returns_404(monkeypatch, tmp_path):
591
+ # 404 means "not ready yet / not built" -- the dialog uses this to keep
592
+ # waiting on the compile rather than treating it as a fleet outage (which
593
+ # would be 502).
594
+ addon = FakeFleetAddon()
595
+ client = _make_client(monkeypatch, tmp_path, addon=addon)
596
+ r = client.get("/fleet/jobs/ghost/firmware")
597
+ assert r.status_code == 404
598
+
599
+
600
+ async def test_fleet_firmware_endpoint_addon_unreachable_returns_502(monkeypatch, tmp_path):
601
+ # Addon returns a 5xx (here, simulated via missing handler -> 404 then
602
+ # mapped to "not available"). For a transport-level failure we'd return
603
+ # 502; cover that explicitly by pointing the client at a transport that
604
+ # always 500s.
605
+ def boom_handler(req: httpx.Request) -> httpx.Response:
606
+ if req.url.path.startswith("/ui/api/jobs/") and req.url.path.endswith(
607
+ "/firmware"
608
+ ):
609
+ return httpx.Response(500, json={"error": "internal"})
610
+ return httpx.Response(404)
611
+
612
+ transport = httpx.MockTransport(boom_handler)
613
+ fc = FleetClient(
614
+ base_url="http://broken-fleet.local", token="tok-123", transport=transport,
615
+ )
616
+
617
+ class _BrokenAddon:
618
+ def make_client(self): return fc
619
+
620
+ client = _make_client(monkeypatch, tmp_path, addon=_BrokenAddon())
621
+ r = client.get("/fleet/jobs/run-1/firmware")
622
+ assert r.status_code == 502
623
+
624
+
491
625
  # ---------------------------------------------------------------------------
492
626
  # SSE log relay
493
627
  # ---------------------------------------------------------------------------
@@ -12,6 +12,9 @@ from starlette.routing import Route
12
12
  from wirestudio.mcp.auth import (
13
13
  DEFAULT_TOKEN_PATH,
14
14
  BearerTokenMiddleware,
15
+ TokenManagedError,
16
+ TokenStore,
17
+ load_token_store,
15
18
  resolve_token,
16
19
  )
17
20
 
@@ -34,7 +37,8 @@ def _build_app(token: str) -> Starlette:
34
37
  Route("/public", _ok),
35
38
  ]
36
39
  )
37
- app.add_middleware(BearerTokenMiddleware, token=token)
40
+ store = TokenStore(token=token, path=Path("/unused"), env_managed=True)
41
+ app.add_middleware(BearerTokenMiddleware, store=store)
38
42
  return app
39
43
 
40
44
 
@@ -115,6 +119,43 @@ def test_resolve_token_generates_and_persists(monkeypatch, tmp_path: Path):
115
119
  assert resolve_token(token_path=file_path) == token
116
120
 
117
121
 
122
+ async def test_middleware_reads_rotated_token_without_restart(tmp_path: Path):
123
+ # Rotation updates the live store; the already-mounted middleware must
124
+ # validate against the new token, not the one captured at startup.
125
+ file_path = tmp_path / "mcp-token"
126
+ file_path.write_text("original")
127
+ store = TokenStore(token="original", path=file_path, env_managed=False)
128
+
129
+ app = Starlette(routes=[Route("/mcp", _ok)])
130
+ app.add_middleware(BearerTokenMiddleware, store=store)
131
+ transport = httpx.ASGITransport(app=app)
132
+ async with httpx.AsyncClient(transport=transport, base_url="http://t") as c:
133
+ new_token = store.rotate()
134
+ r_old = await c.get("/mcp", headers={"Authorization": "Bearer original"})
135
+ r_new = await c.get("/mcp", headers={"Authorization": f"Bearer {new_token}"})
136
+ assert r_old.status_code == 401
137
+ assert r_new.status_code == 200
138
+
139
+
140
+ def test_token_store_rotate_persists_new_token(tmp_path: Path):
141
+ file_path = tmp_path / "mcp-token"
142
+ store = load_token_store(token_path=file_path)
143
+ first = store.token
144
+ rotated = store.rotate()
145
+ assert rotated != first
146
+ assert store.token == rotated
147
+ assert file_path.read_text() == rotated
148
+ assert file_path.stat().st_mode & 0o777 == 0o600
149
+
150
+
151
+ def test_token_store_rotate_refuses_env_managed(monkeypatch, tmp_path: Path):
152
+ monkeypatch.setenv("WIRESTUDIO_MCP_TOKEN", "from-env")
153
+ store = load_token_store(token_path=tmp_path / "mcp-token")
154
+ assert store.env_managed is True
155
+ with pytest.raises(TokenManagedError):
156
+ store.rotate()
157
+
158
+
118
159
  def test_default_token_path_is_under_user_config():
119
160
  # Anchored to ~/.config/wirestudio so operators know where to look
120
161
  # without grepping the source.
@@ -28,6 +28,7 @@ EXPECTED_TOOLS = {
28
28
  "remove_component",
29
29
  "set_param",
30
30
  "set_connection",
31
+ "set_strict",
31
32
  "add_bus",
32
33
  "solve_pins",
33
34
  "kicad_schematic",
@@ -122,6 +123,20 @@ async def test_add_component_persists_to_store(mcp_server):
122
123
  assert saved["components"][0]["id"] == payload["instance_id"]
123
124
 
124
125
 
126
+ async def test_set_strict_persists_and_validate_honors_it(mcp_server):
127
+ server, store = mcp_server
128
+ design_id = _seed_design(store)
129
+
130
+ out = await server.call_tool("set_strict", {"design_id": design_id, "enabled": True})
131
+ assert _content_to_dict(out)["strict"] is True
132
+ # Persisted so every later tool call (and the browser) sees strict on.
133
+ assert store.load(design_id)["strict"] is True
134
+
135
+ out = await server.call_tool("set_strict", {"design_id": design_id, "enabled": False})
136
+ assert _content_to_dict(out)["strict"] is False
137
+ assert store.load(design_id)["strict"] is False
138
+
139
+
125
140
  async def test_validate_against_stored_design(mcp_server):
126
141
  server, store = mcp_server
127
142
  design_id = _seed_design(store)
@@ -4,7 +4,9 @@ import pytest
4
4
  from fastapi.testclient import TestClient
5
5
 
6
6
  from wirestudio.api.app import create_app
7
+ from wirestudio.csp.pin_solver import solve_pins
7
8
  from wirestudio.designs.seed import insert_module
9
+ from wirestudio.generate import render_yaml
8
10
  from wirestudio.generate.ascii_gen import render_ascii
9
11
  from wirestudio.library import default_library
10
12
  from wirestudio.model import Design
@@ -36,6 +38,28 @@ def test_unknown_module_raises():
36
38
  default_library().module("nope")
37
39
 
38
40
 
41
+ @pytest.mark.parametrize("module_id", [m.id for m in default_library().list_modules()])
42
+ @pytest.mark.parametrize("board_id,mcu", [
43
+ ("esp32-devkitc-v4", "esp32"),
44
+ ("wemos-d1-mini", "esp8266"),
45
+ ])
46
+ def test_module_inserts_solves_and_renders(module_id, board_id, mcu):
47
+ """Every bundled module must expand into real components that auto-wire,
48
+ solve to concrete pins with no unresolved connections, and render valid
49
+ ESPHome YAML -- on both an ESP32 and an ESP8266 host."""
50
+ lib = default_library()
51
+ m = lib.module(module_id)
52
+ d = _base_design()
53
+ d["board"] = {"library_id": board_id, "mcu": mcu, "framework": "arduino"}
54
+ d["power"]["budget_ma"] = 1000
55
+ _, d = insert_module(d, lib, m)
56
+ assert len(d["components"]) == len(m.components)
57
+ result = solve_pins(d, lib)
58
+ assert result.unresolved == [], f"{module_id}/{board_id}: {result.unresolved}"
59
+ yaml_out = render_yaml(Design.model_validate(result.design), lib)
60
+ assert yaml_out.strip()
61
+
62
+
39
63
  # --- insert_module ----------------------------------------------------------
40
64
 
41
65
  def test_insert_module_adds_all_components_with_provenance():
@@ -281,3 +281,41 @@ def test_solver_prefers_adc1_over_adc2_for_analog_in(library):
281
281
  # shift as the board YAML evolves; the invariant is "not on ADC2".
282
282
  adc1_pins = {"GPIO32", "GPIO33", "GPIO34", "GPIO35", "GPIO36", "GPIO39"}
283
283
  assert chosen in adc1_pins, f"expected ADC1 pin, got {chosen}"
284
+
285
+
286
+ def _d1_mini_design(components: list[dict], connections: list[dict]) -> dict:
287
+ return {
288
+ "schema_version": "0.1",
289
+ "id": "pwm-pref",
290
+ "name": "PWM pref",
291
+ "board": {"library_id": "wemos-d1-mini", "mcu": "esp8266"},
292
+ "components": components,
293
+ "buses": [],
294
+ "connections": connections,
295
+ }
296
+
297
+
298
+ def test_pwm_output_avoids_no_pwm_pin_and_prefers_pwm(library):
299
+ """A requires_pwm output (pwm_fan.PWM) must never land on ESP8266's D0
300
+ (tagged no_pwm -- GPIO16 has no timer PWM), and should prefer a
301
+ pwm-tagged pin (D5/D6/D7) over an untagged-but-usable gpio."""
302
+ d = _d1_mini_design(
303
+ [{"id": "fan", "library_id": "pwm_fan", "params": {}}],
304
+ [{"component_id": "fan", "pin_role": "PWM", "target": {"kind": "gpio", "pin": ""}}],
305
+ )
306
+ result = solve_pins(d, library)
307
+ chosen = next(a for a in result.assigned if a.pin_role == "PWM").new_target["pin"]
308
+ assert chosen != "D0"
309
+ assert chosen in ("D5", "D6", "D7") # pwm-tagged pins on wemos-d1-mini
310
+
311
+
312
+ def test_interrupt_input_avoids_no_interrupt_pin(library):
313
+ """A requires_interrupt input (pulse_counter.PULSE) must never land on
314
+ ESP8266's D0 (tagged no_interrupt -- GPIO16 can't attachInterrupt)."""
315
+ d = _d1_mini_design(
316
+ [{"id": "flow", "library_id": "pulse_counter", "params": {}}],
317
+ [{"component_id": "flow", "pin_role": "PULSE", "target": {"kind": "gpio", "pin": ""}}],
318
+ )
319
+ result = solve_pins(d, library)
320
+ chosen = next(a for a in result.assigned if a.pin_role == "PULSE").new_target["pin"]
321
+ assert chosen != "D0"
@@ -0,0 +1,165 @@
1
+ """Strict mode: the design-level toggle that makes warnings block generation.
2
+
3
+ Permissive (default) surfaces electrical/CSP violations in warnings but still
4
+ generates. Strict (`design.strict: true`, or a per-call override) refuses to
5
+ generate while any warn/error compatibility entry or design warning remains.
6
+ Covers the shared blocker helper plus every generation surface that honors it:
7
+ the MCP/agent render + validate tools and the CLI.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from pathlib import Path
13
+
14
+ import jsonschema
15
+ import pytest
16
+
17
+ from wirestudio.agent.tools import _run_render, _run_validate
18
+ from wirestudio.csp.compatibility import strict_blockers
19
+ from wirestudio.generate.__main__ import main as generate_main
20
+ from wirestudio.library import default_library
21
+ from wirestudio.model import Design
22
+
23
+ REPO_ROOT = Path(__file__).resolve().parent.parent
24
+ EXAMPLES_DIR = REPO_ROOT / "wirestudio" / "examples"
25
+
26
+
27
+ @pytest.fixture
28
+ def lib():
29
+ return default_library()
30
+
31
+
32
+ def _clean() -> dict:
33
+ """solder-fan is warn/error-clean (it carries one info-level A0
34
+ voltage_limit, which must never block)."""
35
+ return json.loads((EXAMPLES_DIR / "solder-fan.json").read_text())
36
+
37
+
38
+ def _dirty_compat() -> dict:
39
+ """Reroute the fan's PWM to D0 (no_pwm) -> a function_unsupported error."""
40
+ d = _clean()
41
+ for c in d["connections"]:
42
+ if c["component_id"] == "exhaust" and c["pin_role"] == "PWM":
43
+ c["target"] = {"kind": "gpio", "pin": "D0"}
44
+ return d
45
+
46
+
47
+ # --- model / schema ---------------------------------------------------------
48
+
49
+ def test_design_defaults_to_permissive():
50
+ d = Design.model_validate(_clean())
51
+ assert d.strict is False
52
+
53
+
54
+ def test_schema_accepts_strict_flag():
55
+ schema = json.loads((REPO_ROOT / "wirestudio" / "schema" / "design.schema.json").read_text())
56
+ design = _clean()
57
+ design["strict"] = True
58
+ jsonschema.validate(design, schema) # must not raise
59
+
60
+
61
+ # --- strict_blockers helper -------------------------------------------------
62
+
63
+ def test_strict_blockers_empty_for_clean_design(lib):
64
+ assert strict_blockers(_clean(), lib) == []
65
+
66
+
67
+ def test_strict_blockers_flags_compat_warn_error(lib):
68
+ blockers = strict_blockers(_dirty_compat(), lib)
69
+ assert len(blockers) == 1
70
+ assert blockers[0].kind == "compatibility"
71
+ assert blockers[0].severity == "error"
72
+ assert blockers[0].code == "function_unsupported"
73
+
74
+
75
+ def test_strict_blockers_includes_design_warnings(lib):
76
+ d = _clean()
77
+ d["warnings"] = [
78
+ {"level": "error", "code": "demo", "text": "electrical violation"},
79
+ {"level": "info", "code": "note", "text": "just fyi"},
80
+ ]
81
+ blockers = strict_blockers(d, lib)
82
+ # error-level design warning blocks; info-level does not.
83
+ kinds = [(b.kind, b.severity, b.code) for b in blockers]
84
+ assert ("design", "error", "demo") in kinds
85
+ assert all(b.severity != "info" for b in blockers)
86
+
87
+
88
+ def test_strict_blockers_ignores_info_severity(lib):
89
+ """The solder-fan pot on A0 emits an info voltage_limit -- never a blocker."""
90
+ from wirestudio.csp.compatibility import check_pin_compatibility
91
+ compat = check_pin_compatibility(_clean(), lib)
92
+ assert any(w.severity == "info" for w in compat) # the A0 ceiling note
93
+ assert strict_blockers(_clean(), lib) == []
94
+
95
+
96
+ # --- MCP/agent render tool --------------------------------------------------
97
+
98
+ def test_render_permissive_generates_despite_blockers(lib):
99
+ d = _dirty_compat()
100
+ d["strict"] = False
101
+ assert _run_render(d, lib)["ok"] is True
102
+
103
+
104
+ def test_render_strict_blocks_on_dirty_design(lib):
105
+ d = _dirty_compat()
106
+ d["strict"] = True
107
+ r = _run_render(d, lib)
108
+ assert r["ok"] is False
109
+ assert r["strict_blocked"] is True
110
+ assert len(r["blockers"]) == 1
111
+
112
+
113
+ def test_render_strict_passes_clean_design(lib):
114
+ d = _clean()
115
+ d["strict"] = True
116
+ r = _run_render(d, lib)
117
+ assert r["ok"] is True
118
+ assert r["yaml"].startswith("esphome:")
119
+
120
+
121
+ # --- MCP/agent validate tool ------------------------------------------------
122
+
123
+ def test_validate_permissive_ok_but_surfaces_blockers(lib):
124
+ d = _dirty_compat()
125
+ d["strict"] = False
126
+ r = _run_validate(d, lib)
127
+ assert r["ok"] is True # permissive validate stays ok
128
+ assert r["strict"] is False
129
+ assert len(r["blockers"]) == 1 # but the blocker is still shown
130
+
131
+
132
+ def test_validate_strict_fails_on_blockers(lib):
133
+ d = _dirty_compat()
134
+ d["strict"] = True
135
+ r = _run_validate(d, lib)
136
+ assert r["ok"] is False
137
+ assert r["strict"] is True
138
+ assert len(r["blockers"]) == 1
139
+
140
+
141
+ # --- CLI --------------------------------------------------------------------
142
+
143
+ def _write(tmp_path: Path, design: dict) -> Path:
144
+ p = tmp_path / "design.json"
145
+ p.write_text(json.dumps(design))
146
+ return p
147
+
148
+
149
+ def test_cli_strict_design_blocks(tmp_path):
150
+ d = _dirty_compat()
151
+ d["strict"] = True
152
+ assert generate_main([str(_write(tmp_path, d))]) == 1
153
+
154
+
155
+ def test_cli_strict_flag_overrides_permissive_design(tmp_path):
156
+ d = _dirty_compat()
157
+ d["strict"] = False
158
+ assert generate_main([str(_write(tmp_path, d)), "--strict"]) == 1
159
+
160
+
161
+ def test_cli_clean_strict_design_generates(tmp_path, capsys):
162
+ d = _clean()
163
+ d["strict"] = True
164
+ assert generate_main([str(_write(tmp_path, d))]) == 0
165
+ assert "esphome:" in capsys.readouterr().out
@@ -1,3 +1,3 @@
1
1
  """wirestudio: design.json -> ESPHome YAML + ASCII diagram."""
2
2
 
3
- __version__ = "0.17.2"
3
+ __version__ = "0.18.0"