test_client = , app = , mocker = station = ClassicServer(id='yc_station_67', branch_id='', vendor_id='vendor_yc', field_server_id='', station_title='測試站', descri...670': ClassicServerChargerMapItemValue(charger_id='bnl_ocppv16_smart12345670', power_type=None, connector_count=None)}) charger = ClassicCharger(id='bnl_ocppv16_smart12345670', station_id='yc_station_67', charge_station='', name='測試樁') @pytest.mark.asyncio async def test_available(test_client: AsyncClient, app: FastAPI, mocker: MockerFixture, station: ClassicServer, charger: ClassicCharger): _set_auth_override(mocker) _set_redis_mock(app, mocker, _make_charger_info()) app_state = cast(AppState, app.state) config_db = app_state['classic_config_db_engine'] poi_db = app_state['classic_poi_db_engine'] await config_db.save(station) await poi_db.save(charger) try: _set_fee_mock(mocker, fee=8.0, unit=FeeUnitEnum.FEE_UNIT_BY_KWH) > response = await test_client.get(f'/classic/chargers/{CHARGER_ID}') ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ app/tests/test_100_classic_app/test_114_charger.py:142: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ .venv/lib/python3.13/site-packages/httpx/_client.py:1768: in get return await self.request( .venv/lib/python3.13/site-packages/httpx/_client.py:1540: in request return await self.send(request, auth=auth, follow_redirects=follow_redirects) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/httpx/_client.py:1629: in send response = await self._send_handling_auth( .venv/lib/python3.13/site-packages/httpx/_client.py:1657: in _send_handling_auth response = await self._send_handling_redirects( .venv/lib/python3.13/site-packages/httpx/_client.py:1694: in _send_handling_redirects response = await self._send_single_request(request) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/httpx/_client.py:1730: in _send_single_request response = await transport.handle_async_request(request) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/httpx/_transports/asgi.py:170: in handle_async_request await self.app(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/applications.py:1163: in __call__ await super().__call__(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/applications.py:90: in __call__ await self.middleware_stack(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/middleware/errors.py:186: in __call__ raise exc .venv/lib/python3.13/site-packages/starlette/middleware/errors.py:164: in __call__ await self.app(scope, receive, _send) .venv/lib/python3.13/site-packages/starlette/middleware/base.py:198: in __call__ raise app_exc .venv/lib/python3.13/site-packages/starlette/middleware/base.py:144: in coro await self.app(scope, receive_or_disconnect, send_no_error) .venv/lib/python3.13/site-packages/starlette/middleware/cors.py:88: in __call__ await self.app(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/middleware/gzip.py:29: in __call__ await responder(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/middleware/gzip.py:130: in __call__ await super().__call__(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/middleware/gzip.py:46: in __call__ await self.app(scope, receive, self.send_with_compression) .venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py:63: in __call__ await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app raise exc .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app await app(scope, receive, sender) .venv/lib/python3.13/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__ await self.app(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/routing.py:660: in __call__ await self.middleware_stack(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:2683: in app await route.handle(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/routing.py:448: in handle await self.app(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/applications.py:1163: in __call__ await super().__call__(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/applications.py:90: in __call__ await self.middleware_stack(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/middleware/errors.py:186: in __call__ raise exc .venv/lib/python3.13/site-packages/starlette/middleware/errors.py:164: in __call__ await self.app(scope, receive, _send) .venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py:63: in __call__ await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app raise exc .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app await app(scope, receive, sender) .venv/lib/python3.13/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__ await self.app(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/routing.py:660: in __call__ await self.middleware_stack(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:2683: in app await route.handle(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:1753: in handle await self.original_router.handle(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:2738: in handle await included_router._handle_selected(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:1773: in _handle_selected await original_route.handle(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:1264: in handle await app(scope, receive, send) .venv/lib/python3.13/site-packages/fastapi/routing.py:150: in app await wrap_app_handling_exceptions(app, request)(scope, receive, send) .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app raise exc .venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app await app(scope, receive, sender) .venv/lib/python3.13/site-packages/fastapi/routing.py:136: in app response = await f(request) ^^^^^^^^^^^^^^^^ .venv/lib/python3.13/site-packages/fastapi/routing.py:690: in app raw_response = await run_endpoint_function( .venv/lib/python3.13/site-packages/fastapi/routing.py:344: in run_endpoint_function return await dependant.call(**values) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ charger_id = 'bnl_ocppv16_smart12345670', auth_result = {'error_code': , 'member': } app_state = , connector_id = 1 @router.get('/{charger_id}', status_code=status.HTTP_200_OK, operation_id='get_charger') async def get_charger( charger_id: str, auth_result: Annotated[AuthMemberResult, Security(get_authed_and_active_member_di)], app_state: Annotated[AppState, Depends(get_app_state_di)], connector_id: int = Query(1, description='Connector ID'), ) -> GetChargerResponse: ''' 復刻自 new-home GET /api/chargers/{charge_id} - 原本 new-home 的 charger ID 在路徑最後一個 segment,這裡採用 path parameter。 - 成功回應沿用 new-home 慣例之 200。 - 錯誤也沿用 new-home 慣例之 200,不用 4xx、5xx。 - 回應之 `site_info.name` 統一命名為格式 {站名}-{樁名},如果兩個都沒有,就是 unnamed-unnamed。 費率方面: - 原本 new-home 有針對 northsungreen 站做特案處理,這裏沒有。 - 原本 new-home 繞大圈跑層層函式在 Redis 與 DB 抓 smart charging configuration 然後再組一大堆 TOU slot 費率資料,這裏化繁為簡,只從 DB 抓當站當下 smart charging 費率,然後判斷 rate type。 ''' auth_result_code = auth_result['error_code'] if auth_result_code != ClassicErrorCodeEnum.ERROR_OK: return GetChargerResponse(code=auth_result_code) redis_db0_client = app_state['classic_redis_clients']['db0_client'] try: charger_info_bytes = cast(bytes | None, redis_db0_client.hget(RedisDb0HashNameEnum.CHARGER_INFO.value, charger_id)) except Exception as e: logger.error({'title': 'Read charger info from Redis error', 'charger_id': charger_id, 'exception': repr(e)}) return GetChargerResponse( # 沿用 new-home 回應,但這不合理,應該是 `ERROR_CHARGER_NO_EXIST` 或 `ERROR_CHARGER_DISCONNECTED` 較合理。 code=ClassicErrorCodeEnum.ERROR_OK, available=False, site_info=ChargerSiteInfo(name='unnamed', unit='', rate=0), ) > charger_info: dict | None = json.loads(charger_info_bytes.decode()) if charger_info_bytes else None ^^^^^^^^^^^^^^^^^^^^^^^^^ E AttributeError: 'str' object has no attribute 'decode'. Did you mean: 'encode'? app/classic_app/routers/charger_router.py:58: AttributeError