自去年完成物品图片相关功能之后,整个项目基本就处于停滞状态。虽然中途添加了保存头像和配置的功能,但客户端甚至都没有进行适配。

直到国庆,经过我一段时间的努力,终于完成了智慧家庭 0.9.0 版本。在这个版本中我切换了 GraphQL 框架,开始使用 Strawberry,并且支持订阅,能实时更新数据。

其实在很早之前,我就想切换框架,因为之前使用的 Graphene 缺少 维护。不过一直没有去做,主要是想支持新功能,却不知如何下手。

直到我发现 Strawberry 的 某一次 版本更新中,提到可以通过 Django Channels 来实现 GraphQL Subscriptions。我稍微看了看文档,发现特别简单,马上就有了去尝试一下的兴趣。支持 Subscriptions 这个议题,几乎是我刚开始使用 GraphQL 起,就一直开启着。中途尝试了一些,但都由于自己能力有限,没能成功。最后通过轮询来凑合着实现了类似功能,一直用到现在。


在它的 文档 中详细介绍了,如果来给自己的服务器添加这个功能。基本上配置就两步。

  1. 根据 Django Channels 文档,安装依赖,添加 channels 到 INSTALLED_APPS。
  2. 配置 Django 的 asgi.py。加上如下的配置,将 WebSocket 的请求交给 Strawberry 提供的 GraphQLWSConsumer。
websocket_urlpatterns = [
    path("graphql/", AuthMiddlewareStack(GraphQLWSConsumer.as_asgi(schema=schema))),
]

application = ProtocolTypeRouter(
    {
        "http": django_asgi_app,
        "websocket": URLRouter(websocket_urlpatterns),
    }
)

这样就算完成了,GraphQL Subscriptions 的支持。接下来就是添加 Schema。

@gql.type
class Subscription:
    @gql.subscription(permission_classes=[IsAuthenticated])
    async def autowatering_data(
        self,
        info: Info,
        device_id: relay.GlobalID,
    ) -> AsyncGenerator[types.AutowateringData, None]:
        ws = info.context.ws

        # 发送最新的数据
        # 让客户端可以马上显示数据
        try:
            device = await device_id.resolve_node(
                info, ensure_type=Awaitable[models.Device]
            )
        except:
            raise ValidationError("设备不存在")

        last = await sync_to_async(device.data.last)()  # type: ignore
        if last:
            yield last

        async for message in ws.channel_listen(
            "update", groups=[f"autowatering_data.{device.id}"]
        ):
            data = await sync_to_async(AutowateringData.objects.get)(pk=message["pk"])
            yield data

通过 Django Channels 传递实时更新的数据。但是由于序列化的问题,没法直接传递 AutowateringData 实例,所以我传递的 id,然后再通过数据库请求到对应的数据。


在使用这个库的时候,我还同时使用了 strawberry-graphql-djangostrawberry-django-plus,方便我与 Django 集成。

不过在再添加 filter 与 order 的时候还是遇到了一些问题。

  1. 默认排除已删除物品,但只能在提供了 filter 参数的情况下,才会生效(就算参数为空字典也行)。
  2. 排序无法像以前一样使用多个条件(相关议题)。

除这些外,大概还有因为 Django 的异步支持不够好,所以我有很多地方都用到了 sync_to_async,且没有添加 dataloader。等以后 Django 的异步支持完美之后,我就来试试把这些都弄好吧。


使用了 Django Channels 后,我发现支持 WebSocket 不麻烦。在 Channels 中,有一个重要的概念——Consumer。各种事件都将会在这里被处理,包括连接或者离线事件。并且可以通过 channel layer 将一个事件传递给其他 Consumer,也能从其他地方传递给指定 Consumer。只需要 Consumer 指定自己监听的 Groups 即可。

利用 Django Channels 我也将物联网设备与服务器交流的方法从 MQTT + WebHook 切换到了 WebSocket

不过在重写通讯协议的时候,我发现我现在的物联网数据表设计不够好,所有数据都存放到一张表上,这样有很多冗余的数据。比如一段时间里,某一个属性一直都没有变化,但是我却需要在每次更新的时候都存放一次这个数据。或者只有一个属性变化的时候,我就必须将这个变化的数据和其他所有数据一起存一次。

接下来,我的计划就是重新设计物联网数据存储的表,同时完善物联网设备与服务器的通讯协议。

最新的项目结构如图:
smart-home

标签: 智慧家庭, WebSockets